Source code for aenet.geometry.transformations.base

"""
Base classes for structure transformations.

This module provides the abstract base class and chain functionality
for all structure transformations in aenet.
"""

import abc
import logging
from collections.abc import Iterator

from ..structure import AtomicStructure

__author__ = "Alexander Urban, Nongnuch Artrith"
__date__ = "2025-12-01"

# Set up logging
logger = logging.getLogger(__name__)


[docs] class TransformationABC(abc.ABC): """ Abstract base class for all structure transformations. All transformations must implement apply_transformation which yields transformed AtomicStructure objects. """
[docs] @abc.abstractmethod def apply_transformation( self, structure: AtomicStructure, **kwargs ) -> Iterator[AtomicStructure]: """ Apply transformation to structure. Parameters ---------- structure : AtomicStructure Input structure to transform **kwargs Additional keyword arguments for forward compatibility Yields ------ AtomicStructure Transformed structures """ raise NotImplementedError
[docs] class TransformationChain: """ Chain multiple transformations with lazy evaluation. This class allows sequential application of multiple transformations using itertools for memory-efficient streaming. Parameters ---------- transformations : list of TransformationABC List of transformations to apply sequentially Examples -------- >>> chain = TransformationChain([transform1, transform2]) >>> for structure in chain.apply_transformation(input_structure): ... process(structure) >>> # Or consume all at once >>> structures = list(chain.apply_transformation(input_structure)) """
[docs] def __init__(self, transformations: list[TransformationABC]): """Initialize transformation chain.""" self.transformations = transformations logger.info( f"TransformationChain initialized with {len(transformations)} " f"transformations" )
[docs] def apply_transformation( self, structure: AtomicStructure, **kwargs ) -> Iterator[AtomicStructure]: """ Apply all transformations sequentially using depth-first streaming. This avoids iterator aliasing issues and guarantees correct combinatorics (including zero-output steps). Parameters ---------- structure : AtomicStructure Input structure to transform **kwargs Additional keyword arguments passed to transformations Returns ------- Iterator[AtomicStructure] Transformed structures from the chain """ logger.info("Starting chain with 1 input structure") n_steps = len(self.transformations) def _apply_from(step_idx: int, s: AtomicStructure) -> Iterator[AtomicStructure]: # If no more transforms, yield the structure if step_idx >= n_steps: yield s return transform = self.transformations[step_idx] logger.debug( f"Chain step {step_idx+1}/{n_steps}: " f"{transform.__class__.__name__}" ) # For each output of this transform, continue with remaining steps for out in transform.apply_transformation(s, **kwargs): yield from _apply_from(step_idx + 1, out) # One input structure to start with # Return a generator that streams depth-first results return _apply_from(0, structure)