Skip to content

Datasets

samudra.datasets

InferenceDataset(source, prognostic_var_names, boundary_var_names, input_steps, output_steps, normalize_before_mask, masked_fill_value, long_rollout)

Bases: Dataset

Dataset of overlapping windows used for autoregressive inference.

Each window contains input_steps historical states followed by output_steps target states. Consecutive windows advance by output_steps so that every model output becomes the newest part of the next input history. For example::

input_steps=1, output_steps=1: [0 | 1], [1 | 2], [2 | 3]
input_steps=2, output_steps=2: [0, 1 | 2, 3], [2, 3 | 4, 5]
input_steps=2, output_steps=1: [0, 1 | 2], [1, 2 | 3], [2, 3 | 4]

The values before | are inputs and those after it are targets.

Source code in src/samudra/datasets.py
@elapsed
def __init__(
    self,
    source: CanonicalSource,
    prognostic_var_names,
    boundary_var_names,
    input_steps,
    output_steps,
    normalize_before_mask,
    masked_fill_value,
    long_rollout,
):
    super().__init__()
    # NOTE: Keep tensors on CPU during initialization. This allows the dataset
    # to be passed between DataLoader worker processes. Call to(device) before
    # using the dataset for inference.

    self.input_steps = input_steps
    self.output_steps = output_steps
    if not 1 <= self.output_steps <= self.input_steps:
        raise ValueError(
            f"output_steps must be between 1 and {self.input_steps}, "
            f"got {self.output_steps}"
        )
    self.prognostic_var_names = tuple(prognostic_var_names)
    self.boundary_var_names = tuple(boundary_var_names)

    self.num_prognostic_channels = self.input_steps * len(prognostic_var_names)
    self.num_output_channels = self.output_steps * len(prognostic_var_names)
    self.input_resolution = source.resolution
    self._device = torch.device("cpu")
    self._source = source
    self._times = source.time
    self.preprocessor = BatchPreprocessor(
        source,
        self.prognostic_var_names,
        self.boundary_var_names,
        normalize_before_mask=normalize_before_mask,
        masked_fill_value=masked_fill_value,
    )

    window_size = self.input_steps + self.output_steps
    num_windows = (source.time.size - window_size) // self.output_steps + 1
    if num_windows <= 0:
        rolling_values = np.empty((0, window_size), dtype=int)
    else:
        starts = np.arange(num_windows) * self.output_steps
        rolling_values = starts[:, None] + np.arange(window_size)[None, :]
    self.rolling_indices = xr.DataArray(
        rolling_values,
        dims=["window_dim", "time"],
    )

    if long_rollout:
        logger.info(
            f"Long rollout will use input at time {source.time.values[0]} and produce"
            f" output at {source.time.values[self.input_steps]}"
        )

    self.wet_label = source.masks.prognostic_for_steps(self.output_steps)
    self.size = len(self.rolling_indices)

    if using_gpu():
        self.wet_label = self.wet_label.pin_memory()

    # Inference only currently supports the same output resolution as the input
    # resolution.
    self.ctx = BatchGrid(
        self.wet_label, self.input_resolution, self.input_resolution
    )

to(device)

Move the dataset's context tensors to the specified device.

Call this before using the dataset for inference to ensure tensors are on the correct device (GPU).

Source code in src/samudra/datasets.py
def to(self, device: torch.device) -> "InferenceDataset":
    """Move the dataset's context tensors to the specified device.

    Call this before using the dataset for inference to ensure tensors
    are on the correct device (GPU).
    """
    self.ctx = self.ctx.to(device)
    self._device = device
    self.wet_label = self.wet_label.to(device, non_blocking=True)
    return self

get_boundary(step)

Return boundary at the requested step.

Source code in src/samudra/datasets.py
def get_boundary(self, step: int) -> Boundary:
    """Return boundary at the requested step."""
    x_index = self._get_x_index(step)
    boundary = self._get_boundary(x_index)
    return boundary

HostBatch(dataset_id)

Source code in src/samudra/datasets.py
def __init__(self, dataset_id: "TorchTrainDataset.Id"):
    self.dataset_id: TorchTrainDataset.Id = dataset_id
    self.steps: list[RolloutStep] = []
    self.load_stats: LoadStats | None = None

append(input_, boundary, label)

Add a prognostic input, boundary, and prognostic label as the last step.

Source code in src/samudra/datasets.py
def append(
    self,
    input_: torch.Tensor,
    boundary: torch.Tensor,
    label: torch.Tensor,
):
    """Add a prognostic input, boundary, and prognostic label as the last step."""
    self.steps.append(RolloutStep(input_, boundary, label))

ModelBatch(ctx)

A single batch of training data.

A single batch contains multiple steps worth of RolloutStep entries, each of which is a (prognostic_input, boundary_input, label) triple. The prognostic and boundary tensors are carried separately because the samudra-multi model encodes them separately (Samudra just concatenates them later).

Source code in src/samudra/datasets.py
def __init__(self, ctx: BatchGrid):
    self.ctx = ctx
    self.steps: list[RolloutStep] = []
    self.load_stats: LoadStats | None = None

append(prognostic_input, boundary_input, label)

Add another RolloutStep as a new step.

Source code in src/samudra/datasets.py
def append(
    self, prognostic_input: Prognostic, boundary_input: Boundary, label: Prognostic
) -> None:
    """Add another RolloutStep as a new step."""
    self.steps.append(RolloutStep(prognostic_input, boundary_input, label))

__getitem__(step)

Converts index (step) into (prognostic, boundary, label) triple.

Source code in src/samudra/datasets.py
def __getitem__(self, step: int) -> RolloutStep:
    """Converts index (step) into (prognostic, boundary, label) triple."""
    return self.steps[step]

TorchTrainDataset(input_source, label_source, prognostic_var_names, boundary_var_names, input_steps, output_steps, steps, normalize_before_mask, masked_fill_value, stride=1, concurrent_compute_=False)

Bases: Dataset[HostBatch]

This class is used for training and validation.

It creates rolling indices to keep track of histories/past states. But different from InferenceDataset, as it creates rolling indices based on stride. By default, the sliding window / stride is 1.

We make use of ModelBatch class to store a single sample.

For example, Hist=0 ; step=0->[0, 1]; step=1->[1, 2]; step=2->[2, 3]; step=3->[3, 4] Hist=1 ; step=0->[[0, 1], [2, 3]]; step=1->[[2, 3], [4, 5]]; step=2->[[4, 5], [6, 7]]; step=3->[[6, 7], [8, 9]] Hist=2 ; step=0->[[0, 1, 2], [3, 4, 5]]; step=1->[[3, 4, 5], [6, 7, 8]]; step=2->[[6, 7, 8], [9, 10, 11]]; step=3->[[9, 10, 11], [12, 13, 14]]

Source code in src/samudra/datasets.py
@elapsed
def __init__(
    self,
    input_source: CanonicalSource,
    label_source: CanonicalSource | None,
    prognostic_var_names: PrognosticVarNames,
    boundary_var_names: BoundaryVarNames,
    input_steps: int,
    output_steps: int,
    steps: int,
    normalize_before_mask: bool,
    masked_fill_value: float,
    stride: int = 1,
    concurrent_compute_: bool = False,
):
    super().__init__()
    self.id = f"{self.__class__.__name__}_{str(id(self))}"
    sources = [input_source, label_source] if label_source else [input_source]

    self.input_steps = input_steps
    self.output_steps = output_steps
    if not 1 <= self.output_steps <= self.input_steps:
        raise ValueError(
            f"output_steps must be between 1 and {self.input_steps}, "
            f"got {self.output_steps}"
        )
    self.steps: int = steps
    self.stride: int = stride
    self._concurrent_compute = concurrent_compute_

    assert np.array_equal(sources[0].time, sources[-1].time), (
        "Input and label sources have different time slices!"
    )
    time_ = input_source.time
    self.sources = sources
    self.prognostic_var_names = tuple(prognostic_var_names)
    self.boundary_var_names = tuple(boundary_var_names)
    self.num_prognostic_channels: int = self.input_steps * len(prognostic_var_names)
    self.num_output_channels: int = self.output_steps * len(prognostic_var_names)
    self.num_boundary_channels: int = self.input_steps * len(boundary_var_names)

    # This class will be used only for training and validation
    total_steps = self.input_steps + self.output_steps

    # Calculate the number of windows
    num_windows = time_.size - (total_steps - 1) * self.stride

    # Create base indices
    indices = np.arange(num_windows)
    indices_da = xr.DataArray(indices, dims=["window"])

    # Create window dimension
    window_dim = xr.DataArray(np.arange(total_steps), dims=["time"])

    # Construct rolling indices
    self.rolling_indices: Float[xr.DataArray, "window time"] = (
        indices_da + stride * window_dim
    )

    self.preprocessors = [
        BatchPreprocessor(
            source,
            self.prognostic_var_names,
            self.boundary_var_names,
            normalize_before_mask=normalize_before_mask,
            masked_fill_value=masked_fill_value,
        )
        for source in sources
    ]

    self.ctx = BatchGrid(
        label_mask=self.sources[-1].masks.prognostic_for_steps(self.output_steps),
        input_resolution_cpu=self.sources[0].resolution,
        output_resolution_cpu=self.sources[-1].resolution,
    )

    self.size: int = (
        time_.size
        - self.steps * self.output_steps * self.stride
        - (self.input_steps - 1) * self.stride
    )

to_model_batch(host_batch, device)

Convert HostBatch to ModelBatch, moving tensors to the specified device.

Parameters:

Name Type Description Default
host_batch HostBatch

CPU data from worker process

required
device device

Target device (typically GPU) to move tensors to

required

Returns:

Type Description
ModelBatch

ModelBatch with tensors on the target device

Source code in src/samudra/datasets.py
def to_model_batch(self, host_batch: HostBatch, device: torch.device) -> ModelBatch:
    """Convert HostBatch to ModelBatch, moving tensors to the specified device.

    Args:
        host_batch: CPU data from worker process
        device: Target device (typically GPU) to move tensors to

    Returns:
        ModelBatch with tensors on the target device
    """
    model_batch = ModelBatch(self.ctx.to(device))
    for input_, boundary, label in host_batch.steps:
        prog_input = self.preprocessors[0].prepare_prognostic(input_, device)
        boundary_input = self.preprocessors[0].prepare_boundary(boundary, device)
        label_tensor = self.preprocessors[-1].prepare_prognostic(label, device)
        model_batch.append(prog_input, boundary_input, label_tensor)
    model_batch.load_stats = host_batch.load_stats
    return model_batch

BatchLoader(host_loader, datasets, device)

Wrapper around a torch DataLoader that handles GPU post-processing.

This class wraps a DataLoader[HostBatch] and converts the raw data to ModelBatch by applying GPU-based normalization and masking. This allows the data loading process to handle I/O while the main process handles GPU operations.

Since the data samples flow from one process to the other, we want to tie them back to the dataset they came from which knows how to do that second half once they're in the main process which has GPU access set up. To do that, each data sample (which could come from a different dataset) has a dataset ID -- datasets maps from those IDs to the original datasets.

Source code in src/samudra/datasets.py
def __init__(
    self,
    host_loader: torch.utils.data.DataLoader[HostBatch],
    datasets: list[TorchTrainDataset],
    device: torch.device,
):
    self._host_loader = host_loader
    self._datasets = {dataset.id: dataset for dataset in datasets}
    self._device = device

__iter__()

Iterate over the dataloader, converting HostBatch to ModelBatch.

Source code in src/samudra/datasets.py
def __iter__(self):
    """Iterate over the dataloader, converting HostBatch to ModelBatch."""
    for host_batch in self._host_loader:
        dataset = self._datasets[host_batch.dataset_id]
        model_batch = dataset.to_model_batch(host_batch, self._device)
        yield model_batch

__getitem__(index)

Access a single item by index, converting HostBatch to ModelBatch.

Note: This bypasses the DataLoader's sampling/batching and directly accesses the underlying dataset for test purposes.

Source code in src/samudra/datasets.py
def __getitem__(self, index: int) -> ModelBatch:
    """Access a single item by index, converting HostBatch to ModelBatch.

    Note: This bypasses the DataLoader's sampling/batching and directly accesses
    the underlying dataset for test purposes.
    """
    # Access the underlying dataset directly
    host_batch = self._host_loader.dataset[index]
    # Apply the collate function to add batch dimension (expects a list)
    collate_fn = self._host_loader.collate_fn
    if collate_fn is not None:
        host_batch = collate_fn([host_batch])
    # Get the dataset that created this raw data
    dataset = self._datasets[host_batch.dataset_id]
    # Convert to ModelBatch
    model_batch = dataset.to_model_batch(host_batch, self._device)
    return model_batch