Source code for omniloader.transforms.base
"""Transform base classes and shared helpers.
A :class:`Transform` operates on an already-unified sample dict (the output of
:class:`~omniloader.schema.unify.SampleUnifier`) and is modality-agnostic — it
reasons only about vectors, sequences and their ``<name>_mask`` companions. Each
transform is:
* **seedable** — an optional :class:`torch.Generator` makes every draw
reproducible;
* **mask-aware** — values are only altered where the mask is ``True`` (padding
and placeholders are left untouched);
* **train/eval-aware** — augmentations set ``train_only = True`` so they are
skipped during evaluation, while normalization runs in both phases.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
import torch
if TYPE_CHECKING:
from collections.abc import Sequence
from omniloader.schema.spec import UnifiedSchema
#: A unified sample dict mapping keys (and ``<name>_mask``) to tensors/metadata.
Sample = dict[str, Any]
def _rand(generator: torch.Generator | None) -> float:
"""Draw a single uniform ``[0, 1)`` scalar, optionally from a generator."""
return float(torch.rand((), generator=generator).item())
[docs]
class Compose(Transform):
"""Apply a list of transforms in order.
Args:
transforms: The transforms to apply sequentially.
"""
def __init__(self, transforms: Sequence[Transform]) -> None:
self.transforms = list(transforms)
[docs]
def __call__(
self,
sample: Sample,
*,
training: bool = True,
generator: torch.Generator | None = None,
) -> Sample:
"""Apply each contained transform in order (each honours its own gate)."""
for transform in self.transforms:
sample = transform(sample, training=training, generator=generator)
return sample
[docs]
def apply(self, sample: Sample, generator: torch.Generator | None) -> Sample:
"""Apply every contained transform (train gate handled per-transform)."""
for transform in self.transforms:
sample = transform.apply(sample, generator)
return sample
[docs]
def broadcast_mask(mask: torch.Tensor, value: torch.Tensor) -> torch.Tensor:
"""Broadcast a validity mask to the value's shape for ``torch.where``.
Args:
mask: Mask of shape ``()`` (vector) or ``(T,)`` (sequence).
value: Value of shape ``()``, ``(F,)``, ``(T,)`` or ``(T, F)``.
Returns:
A boolean tensor broadcastable to ``value.shape``.
"""
if mask.ndim < value.ndim:
# Sequence value (T, F) with a (T,) mask -> add trailing feature axis.
mask = mask.reshape(mask.shape + (1,) * (value.ndim - mask.ndim))
return mask.expand_as(value)
[docs]
def resolve_keys(keys: Sequence[str] | None, schema: UnifiedSchema | None) -> list[str]:
"""Return explicit ``keys`` or all feature keys from ``schema``."""
if keys is not None:
return list(keys)
if schema is None:
raise ValueError("Either keys or a schema must be provided")
return list(schema.feature_keys)
[docs]
def resolve_sequence_keys(keys: Sequence[str] | None, schema: UnifiedSchema | None) -> list[str]:
"""Return explicit ``keys`` or all sequence feature keys from ``schema``."""
if keys is not None:
return list(keys)
if schema is None:
raise ValueError("Either keys or a schema must be provided")
return [spec.name for spec in schema.features if spec.is_sequence]