Basic Structure Transformations
The aenet.geometry.transformations module provides tools for generating
structural variations from input structures. This is useful for:
Creating training data for machine learning potentials
Exploring configuration space around a reference structure
Generating diverse structural datasets
Testing model robustness to structural perturbations
Basic Concepts
Transformations take an input structure and yield transformed structures using
Python’s iterator protocol. All transformations work with
aenet.geometry.AtomicStructure objects.
Key terminology:
Deterministic transformation: Always produces the same output for a given input
Stochastic transformation: Uses randomness; output varies unless a seed is provided
Iterator-Based Design
All transformations return iterators, which provides memory efficiency and flexibility:
>>> import itertools
>>> from aenet.geometry import AtomicStructure
>>> from aenet.geometry.transformations import AtomDisplacementTransformation
>>> structure = AtomicStructure(
... [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]],
... ["Si", "O"],
... avec=[[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]],
... )
>>> transform = AtomDisplacementTransformation(displacement=0.05)
>>> all_structures = list(transform.apply_transformation(structure))
>>> len(all_structures)
6
>>> first_two = list(itertools.islice(
... transform.apply_transformation(structure), 2
... ))
>>> len(first_two)
2
>>> first_two[0].coords[-1][0].tolist()
[0.05, 0.0, 0.0]
The iterator pattern allows you to:
Process structures one at a time without loading all into memory
Stop early if you find what you need
Compose with other iterators using
itertools
Atom Displacement
AtomDisplacementTransformation displaces each atom in Cartesian
x, y, and z directions. For a structure with N atoms, this generates 3N
output structures.
Use cases:
Finite difference calculation of forces
Exploring local energy landscape
Testing force field sensitivity
Example:
>>> from aenet.geometry import AtomicStructure
>>> from aenet.geometry.transformations import AtomDisplacementTransformation
>>> structure = AtomicStructure(
... [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]],
... ["Si", "O"],
... avec=[[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]],
... )
>>> transform = AtomDisplacementTransformation(displacement=0.05)
>>> displaced_structures = list(transform.apply_transformation(structure))
>>> len(displaced_structures)
6
>>> displaced_structures[3].coords[-1][1].tolist()
[1.05, 1.0, 1.0]
The displacement magnitude is in Angstroms and should be small enough to remain in the harmonic regime for force calculations (typically 0.01-0.1 Å).
Cell Volume Scaling
CellVolumeTransformation uniformly scales the unit cell volume
while preserving fractional coordinates. The scaling is controlled by
percentage changes in the lattice-vector scale factor, so the cell volume
changes cubically with that scale.
All transformations on this page that modify the unit cell preserve
fractional coordinates. Since aenet.geometry.AtomicStructure
stores Cartesian coordinates, the Cartesian positions are recomputed
from the updated cell, and copied energy/force labels are cleared on
the generated structures because they are stale after deformation.
Use cases:
Equation of state calculations
Pressure-volume relationships
Testing volume-dependent properties
Example:
from aenet.geometry.transformations import CellVolumeTransformation
# Scale volume from -5% to +5% in 5 steps
transform = CellVolumeTransformation(
min_percent=-5.0,
max_percent=5.0,
steps=5
)
original_volume = structure.cellvolume()
for i, s in enumerate(transform.apply_transformation(structure)):
new_volume = s.cellvolume()
percent_change = 100 * (new_volume - original_volume) / original_volume
print(f"Structure {i}: V = {new_volume:.2f} ų ({percent_change:+.1f}%)")
Physics: The lattice vectors scale uniformly by \(s = (1 + p/100)\), where \(p\) is the user-specified percentage. The resulting volume therefore follows \(V_{\text{new}} = V_{\text{old}} \times s^3\).
Isovolumetric Strain
IsovolumetricStrainTransformation applies strain along one lattice
direction while adjusting the other two directions to preserve volume. This
is useful for exploring shape changes at constant volume.
Use cases:
Constant-volume optimization
Studying anisotropic mechanical properties
Shape-dependent property calculations
Example:
from aenet.geometry.transformations import IsovolumetricStrainTransformation
# Strain direction 1 (a-axis) from 0.9× to 1.1× original length
transform = IsovolumetricStrainTransformation(
direction=1, # 1=a, 2=b, 3=c
len_min=0.9,
len_max=1.1,
steps=5
)
original_volume = structure.cellvolume()
for i, s in enumerate(transform.apply_transformation(structure)):
new_volume = s.cellvolume()
volume_error = abs(new_volume - original_volume)
print(f"Structure {i}: ΔV = {volume_error:.2e} ų")
Physics: When direction \(i\) is scaled by \(s\), the orthogonal directions are scaled by \(s_\perp = s^{-1/2}\) to maintain \(\det(\mathbf{M}) = 1\), where \(\mathbf{M}\) is the transformation matrix. Volume is conserved within numerical tolerance (typically < 10⁻⁵ ų).
Shear Strain
ShearStrainTransformation applies shear strain to a crystal, which
preserves volume (determinant = 1) but changes the cell shape.
Use cases:
Studying elastic properties
Calculating shear moduli
Exploring slip systems
Example:
from aenet.geometry.transformations import ShearStrainTransformation
# Apply shear on xy plane from -0.1 to +0.1
transform = ShearStrainTransformation(
direction=1, # 1=xy, 2=xz, 3=yz
shear_min=-0.1,
shear_max=0.1,
steps=5
)
sheared_cells = [
s.avec[-1]
for s in transform.apply_transformation(structure)
]
Physics: The shear matrix for xy shear is:
where \(\gamma\) is the shear strain parameter. The determinant is always 1, ensuring volume conservation.
Practical Tips
Choosing displacement magnitudes:
For forces: 0.01-0.05 Å (harmonic regime)
For structure search: 0.1-0.3 Å (exploration)
For large perturbations: > 0.5 Å (may need relaxation)
Choosing strain ranges:
Elastic regime: ±2% strain
Beyond elasticity: ±5-10% strain
Phase changes: > ±10% strain
Limiting output:
For large systems, use itertools.islice() to limit structures:
import itertools
# Get only first 100 structures
limited = list(itertools.islice(
transform.apply_transformation(structure), 100
))
See Advanced Structure Transformations for transformation chains and stochastic transformations.
Common Patterns
Save all structures:
for i, s in enumerate(transform.apply_transformation(structure)):
s.to_file(f'output_{i:04d}.xsf')
Filter structures:
# Only keep structures with energy below threshold
good_structures = [
s for s in transform.apply_transformation(structure)
if s.energy[-1] is not None and s.energy[-1] < threshold
]
Combine with list comprehension:
volumes = [
s.cellvolume()
for s in transform.apply_transformation(structure)
]
Next Steps
For transformation chains and stochastic transformations, see Advanced Structure Transformations
For complete API documentation, see Structure Transformations