# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: OpenMDW-1.1 from collections.abc import Iterable from typing import Any import torch from cosmos_framework.utils.generator.image_resize import get_vision_data_resolution as get_vision_data_resolution def read_positive_int_metadata( data_batch: dict[str, Any], key: str, expected_count: int, ) -> list[int] | None: """Normalize positive integer metadata from common dataloader representations. Metadata produced by the dataset can reach consumers in several equivalent forms: a tensor containing one value per sample after ``default_collate``, a Python integer for an unbatched sample, or a list of scalar integers/tensors. Iterative and joint dataloaders may additionally wrap each scalar entry in one or more single-element lists. This helper normalizes all of those forms into one integer per expected sample while validating the metadata contract. Args: data_batch: Batch containing the metadata. key: Metadata key to read. expected_count: Required number of scalar values after normalization. Returns: A list of positive integers, or ``None`` when ``key`` is absent. Raises: TypeError: If the metadata contains an unsupported value type. ValueError: If a list wrapper is not a singleton, the number of values differs from ``expected_count``, or any value is not positive. """ raw_value = data_batch.get(key) if raw_value is None: return None if isinstance(raw_value, torch.Tensor): flattened = raw_value.reshape(-1) # [N] entries: list[Any] = list(flattened) elif isinstance(raw_value, list): entries = raw_value elif isinstance(raw_value, int): entries = [raw_value] else: raise TypeError(f"{key} must be a tensor, integer, or list, got {type(raw_value).__name__}.") values: list[int] = [] for entry in entries: while isinstance(entry, list): if len(entry) != 1: raise ValueError(f"{key} entries must contain one value, got {entry}.") entry = entry[0] if isinstance(entry, torch.Tensor): try: value = int(entry.item()) except RuntimeError as error: raise ValueError(f"{key} entries must be scalar, got shape {tuple(entry.shape)}.") from error elif isinstance(entry, int): value = entry else: raise TypeError(f"{key} entries must be tensors or integers, got {type(entry).__name__}.") values.append(value) if len(values) != expected_count: raise ValueError(f"{key} must have {expected_count} values, got {len(values)}.") if any(value <= 0 for value in values): raise ValueError(f"{key} values must be positive, got {values}.") return values def slice_data_batch( data_batch: dict[str, Any], start: int, limit: int, multi_item_fields: Iterable[str] = ("image", "images", "video", "videos", "image_size"), ) -> dict[str, Any]: """Slice a data batch based on the start and limit indices. For most fields, the slice ``[start:limit]`` is applied directly along the sample dimension. For fields listed in ``multi_item_fields`` (e.g. ``image``, ``images``, ``video``, and ``videos``), each sample may contribute multiple visual items that are concatenated in flat order. In that case, when ``num_vision_items_per_sample`` is present in ``data_batch``, the slice is expanded to cover all visual items belonging to the requested samples. Example: ``num_vision_items_per_sample = [2, 2]`` and ``video = [v1_s1, v2_s1, v1_s2, v2_s2]``. Slicing with ``start=0, limit=1`` returns ``video = [v1_s1, v2_s1]``. The ``lidar`` field is flattened the same way and follows ``num_lidar_items_per_sample``, since a sample's range clips need not be as many as its camera clips. Args: data_batch: The data batch to slice. start: The start sample index (inclusive). limit: The end sample index (exclusive). multi_item_fields: Field names whose values store multiple visual items per sample concatenated in flat order. Only used when ``data_batch`` contains ``num_vision_items_per_sample``. Returns: The sliced data batch. """ assert start >= 0 and limit > 0, "Start and limit must be positive" assert start < limit, "Start must be less than limit" def flat_range(counts: Any) -> tuple[int, int]: counts_list = counts.tolist() if isinstance(counts, torch.Tensor) else list(counts) return sum(counts_list[:start]), sum(counts_list[:limit]) num_items = data_batch.get("num_vision_items_per_sample") flat_start, flat_limit = flat_range(num_items) if num_items is not None else (start, limit) # The LiDAR stream is flattened the same way, and counts its own items. num_lidar_items = data_batch.get("num_lidar_items_per_sample") lidar_start, lidar_limit = flat_range(num_lidar_items) if num_lidar_items is not None else (start, limit) multi_item_fields = set(multi_item_fields) sliced_batch = {} for key, value in data_batch.items(): if key in multi_item_fields and num_items is not None: s, e = flat_start, flat_limit elif key == "lidar" and num_lidar_items is not None: s, e = lidar_start, lidar_limit else: s, e = start, limit if isinstance(value, torch.Tensor): sliced_batch[key] = value[s:e] elif isinstance(value, list): sliced_batch[key] = value[s:e] else: sliced_batch[key] = value return sliced_batch