Source code for atomworks.ml.datasets.file_dataset
"""File-based dataset implementation."""
from collections.abc import Callable
from os import PathLike
from pathlib import Path
from typing import Any
from atomworks.ml.utils.io import scan_directory
from .base import MolecularDataset
def _always_true(x: PathLike) -> bool:
return True
[docs]
class FileDataset(MolecularDataset):
"""Dataset that loads molecular data from individual files.
Each file represents one example in the dataset. If creating a dataset from a
directory, use the :meth:`from_directory` class method instead of the default
constructor.
"""
def __init__(
self,
*,
file_paths: list[str | PathLike],
name: str,
filter_fn: Callable[[PathLike], bool] | None = None,
**kwargs: Any,
):
"""Initialize FileDataset.
Args:
file_paths: List of file paths for the dataset. Each file represents
one example.
name: Descriptive name for this dataset. Used for debugging and some
downstream functions when using nested datasets.
filter_fn: Optional function to filter file paths. Should return True
for files to include.
**kwargs: Additional arguments passed to :class:`MolecularDataset`.
Examples:
Create from explicit file list:
>>> files = ["/path/to/file1.cif", "/path/to/file2.cif"]
>>> dataset = FileDataset(file_paths=files, name="my_dataset")
"""
super().__init__(name=name, **kwargs)
self.filter_fn = filter_fn or _always_true
# Convert to Path objects and filter
file_paths = [Path(path) for path in file_paths if self.filter_fn(path)]
if not file_paths:
raise ValueError("No files found after applying filters")
if len(file_paths) != len(set(file_paths)):
raise ValueError("File paths must be unique")
# Sort for consistent id<>idx mapping
file_paths.sort()
self.file_paths = file_paths
# Create ID mapping
self.id_to_idx_map = {self._get_example_id(i): i for i, _ in enumerate(self.file_paths)}
# Verify that all example IDs are unique
if len(self.id_to_idx_map) != len(self.file_paths):
raise ValueError("Example IDs must be unique. Found duplicate example IDs.")
[docs]
@classmethod
def from_directory(
cls,
*,
directory: PathLike,
name: str,
max_depth: int = 3,
**kwargs: Any,
) -> "FileDataset":
"""Create a FileDataset by scanning a directory for files.
Args:
directory: Path to directory to scan for files.
name: Descriptive name for this dataset.
max_depth: Maximum depth to scan for files in subdirectories.
**kwargs: Additional arguments passed to :class:`FileDataset`.
Returns:
FileDataset instance with files discovered from the directory.
Example:
Create from directory:
>>> dataset = FileDataset.from_directory(directory="/path/to/files", name="my_dataset", max_depth=2)
"""
dir_path = Path(directory)
if not dir_path.exists():
raise FileNotFoundError(f"Directory {directory} does not exist.")
if not dir_path.is_dir():
raise ValueError(f"Path {directory} is not a directory.")
file_paths = scan_directory(dir_path=dir_path, max_depth=max_depth)
return cls(file_paths=file_paths, name=name, **kwargs)
[docs]
@classmethod
def from_file_list(
cls,
*,
file_paths: list[str | PathLike],
name: str,
**kwargs: Any,
) -> "FileDataset":
"""Create a FileDataset from an explicit list of file paths.
This is an alias for the main constructor for clarity and consistency
with :meth:`from_directory`.
Args:
file_paths: List of file paths for the dataset. Each file represents one example.
name: Descriptive name for this dataset.
**kwargs: Additional arguments passed to :class:`FileDataset`.
Returns:
FileDataset instance with the provided file paths.
"""
return cls(file_paths=file_paths, name=name, **kwargs)
def __len__(self) -> int:
"""Return the number of files in the dataset."""
return len(self.file_paths)
def __contains__(self, example_id: str) -> bool:
"""Check if the dataset contains the example ID."""
return example_id in self.id_to_idx_map
[docs]
def id_to_idx(self, example_id: str | list[str]) -> int | list[int]:
"""Convert example ID(s) to index(es)."""
if isinstance(example_id, list):
return [self.id_to_idx_map[id] for id in example_id]
return self.id_to_idx_map[example_id]
[docs]
def idx_to_id(self, idx: int | list[int]) -> str | list[str]:
"""Convert index(es) to example ID(s)."""
if isinstance(idx, list):
return [self._get_example_id(i) for i in idx]
return self._get_example_id(idx)
def __getitem__(self, idx: int) -> Any:
"""Load and transform an example by file index.
Args:
idx: The index of the file to load.
Returns:
Transformed data from the file.
"""
file_path = str(self.file_paths[idx])
example_id = self._get_example_id(idx)
data = self._apply_loader(file_path)
return self._apply_transform(data, example_id=example_id, idx=idx)
def _get_example_id(self, idx: int) -> str:
"""Get example ID from index - returns filename stem without extensions.
Args:
idx: The index of the file.
Returns:
Filename stem without any extensions.
"""
file_path = self.file_paths[idx]
filename = Path(file_path).stem
# If filename has multiple extensions (e.g., .cif.gz), remove them all
while "." in filename:
filename = Path(filename).stem
return filename