Source code for personalitylinmult.data.datamodule
"""Lightning ``DataModule`` over the built FI HDF5 file."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import lightning as L
import torch
from torch.utils.data import DataLoader
from personalitylinmult.data.collate import collate_fi
from personalitylinmult.data.dataset import FiHdf5Dataset
if TYPE_CHECKING:
from personalitylinmult.train.config import DataConfig
logger = logging.getLogger(__name__)
"""Module-level logger."""
[docs]
def seed_worker(worker_id: int) -> None: # noqa: ARG001 -- worker_init_fn's fixed signature
"""Seed a dataloader worker's RNGs from torch's per-worker seed.
Without this, every worker inherits the parent's numpy seed and augmentation
draws repeat across workers.
Args:
worker_id (int): Worker index, unused but required by the hook.
"""
import random
import numpy as np
seed = torch.initial_seed() % 2**32
np.random.seed(seed)
random.seed(seed)
[docs]
class FiDataModule(L.LightningDataModule):
"""Serves the FI splits from the single HDF5 artifact.
Args:
config (DataConfig): Dataloader configuration.
target_names (list[str]): Label columns for the active task, in head
order.
"""
def __init__(self, config: DataConfig, target_names: list[str]):
super().__init__()
self.config = config
self.target_names = list(target_names)
self.train_dataset: FiHdf5Dataset | None = None
self.valid_dataset: FiHdf5Dataset | None = None
self.test_dataset: FiHdf5Dataset | None = None
def _build(self, subset: str) -> FiHdf5Dataset:
"""Construct one split's dataset.
Args:
subset (str): Split name.
Returns:
FiHdf5Dataset: The dataset.
"""
return FiHdf5Dataset(
h5_path=self.config.h5_path,
subset=subset,
feature_names=self.config.features,
target_names=self.target_names,
window_seconds=self.config.window_seconds,
crop_mode=self.config.crop_mode,
seed=self.config.seed,
)
[docs]
def setup(self, stage: str | None = None) -> None:
"""Construct the datasets needed for a stage.
Args:
stage (str | None): ``"fit"``, ``"validate"``, ``"test"``, or
``None`` for all.
"""
if stage in (None, "fit"):
self.train_dataset = self._build("train")
self.valid_dataset = self._build("valid")
if stage in (None, "validate") and self.valid_dataset is None:
self.valid_dataset = self._build("valid")
if stage in (None, "test"):
self.test_dataset = self._build("test")
def _loader(self, dataset: FiHdf5Dataset, shuffle: bool) -> DataLoader:
"""Wrap a dataset in a configured ``DataLoader``.
Args:
dataset (FiHdf5Dataset): The dataset.
shuffle (bool): Whether to shuffle (training only).
Returns:
DataLoader: The loader.
"""
generator = torch.Generator()
generator.manual_seed(self.config.seed)
return DataLoader(
dataset,
batch_size=self.config.batch_size,
shuffle=shuffle,
num_workers=self.config.num_workers,
# Pinned (page-locked) host memory only speeds up CUDA transfers;
# on MPS/CPU it is a silent no-op that PyTorch warns about on every
# run. The config's pin_memory is the user's *intent* -- it is only
# ever actually honoured when CUDA is the device that would benefit.
pin_memory=self.config.pin_memory and torch.cuda.is_available(),
# Workers reopen their HDF5 handle on start, so tearing them down
# every epoch would pay that cost repeatedly.
persistent_workers=self.config.persistent_workers and self.config.num_workers > 0,
prefetch_factor=self.config.prefetch_factor if self.config.num_workers > 0 else None,
drop_last=shuffle,
collate_fn=collate_fi,
worker_init_fn=seed_worker,
generator=generator,
)
[docs]
def train_dataloader(self) -> DataLoader:
"""Training loader.
Returns:
DataLoader: Shuffled, drop-last loader over the train split.
Raises:
RuntimeError: If called before ``setup``.
"""
if self.train_dataset is None:
raise RuntimeError("setup('fit') must run before train_dataloader().")
return self._loader(self.train_dataset, shuffle=True)
[docs]
def val_dataloader(self) -> DataLoader:
"""Validation loader.
Returns:
DataLoader: Deterministic loader over the valid split.
Raises:
RuntimeError: If called before ``setup``.
"""
if self.valid_dataset is None:
raise RuntimeError("setup('fit') must run before val_dataloader().")
return self._loader(self.valid_dataset, shuffle=False)
[docs]
def test_dataloader(self) -> DataLoader:
"""Test loader.
Returns:
DataLoader: Deterministic loader over the test split.
Raises:
RuntimeError: If called before ``setup``.
"""
if self.test_dataset is None:
raise RuntimeError("setup('test') must run before test_dataloader().")
return self._loader(self.test_dataset, shuffle=False)