"""Builder pattern implementation for site_analysis.
This module provides a fluent builder interface for creating site analysis
objects, making it easier to set up and run site analysis workflows.
Examples::
# Using the builder directly
trajectory = (TrajectoryBuilder()
.with_structure(structure)
.with_mobile_species("Li")
.with_spherical_sites(
centres=[[0.5, 0.5, 0.5], [0.0, 0.0, 0.0]],
radii=2.0,
labels=["octahedral", "tetrahedral"]
)
.build())
# Using a factory function
trajectory = create_trajectory_with_spherical_sites(
structure=structure,
mobile_species="Li",
centres=[[0.5, 0.5, 0.5], [0.0, 0.0, 0.0]],
radii=2.0,
labels=["octahedral", "tetrahedral"]
)
# Creating a trajectory with polyhedral sites
trajectory = create_trajectory_with_polyhedral_sites(
structure=target_structure,
reference_structure=reference_structure,
mobile_species="Li",
centre_species="O",
vertex_species=["Li", "Na"],
cutoff=3.0,
n_vertices=4,
label="tetrahedral"
)
"""
from __future__ import annotations
from pymatgen.core import Structure
from site_analysis.atom import Atom, atoms_from_structure
from site_analysis.site import Site
from site_analysis.spherical_site import SphericalSite
from site_analysis.voronoi_site import VoronoiSite
from site_analysis.polyhedral_site import PolyhedralSite
from site_analysis.dynamic_voronoi_site import DynamicVoronoiSite
from site_analysis.trajectory import Trajectory
from site_analysis.reference_workflow.reference_based_sites import ReferenceBasedSites
import numpy as np
from typing import cast, Callable, Sequence, Any
[docs]
class TrajectoryBuilder:
"""Builder for creating Trajectory objects for site analysis.
This class provides a step-by-step approach to creating a Trajectory
object for analysing site occupations in crystal structures.
**Structure Alignment and Site Mapping**
The builder supports separate control over structure alignment (finding optimal
translations) and site mapping (identifying corresponding sites):
- Structure alignment: Use ``with_structure_alignment()`` to control whether
and how structures are aligned before site creation.
- Site mapping: Use ``with_site_mapping()`` to specify which species are used
to identify corresponding sites between structures.
**Default Behaviors**
- Alignment is enabled by default.
- If mapping species are specified but alignment species are not, mapping
species will be used for alignment.
- If alignment species are specified but mapping species are not, alignment
species will be used for mapping.
Example::
builder = TrajectoryBuilder()
builder.with_structure(structure)
.with_reference_structure(reference_structure)
.with_mobile_species("Li")
.with_structure_alignment(align=True, align_species=["O"])
.with_site_mapping(mapping_species=["Na"])
.with_polyhedral_sites(
centre_species="Li",
vertex_species="O",
cutoff=2.0,
n_vertices=4
)
trajectory = builder.build()
"""
def __init__(self) -> None:
"""Initialize a TrajectoryBuilder.
Creates a new builder with all attributes set to their default values.
"""
# Call reset() to set all attributes to their default values
self.reset()
[docs]
def reset(self) -> 'TrajectoryBuilder':
"""Reset the builder state to default values.
This method clears all configuration and returns the builder to its
initial state. It is called automatically during initialization and
after build(), but can also be called explicitly if needed.
Returns:
self: For method chaining
"""
self._structure: Structure | None = None
self._reference_structure: Structure | None= None
self._mobile_species: str | list[str] | None = None
self._atoms: list[Atom] | None = None
# Alignment options
self._align = True
self._align_species: list[str] | None = None
self._align_metric = 'rmsd'
self._align_algorithm = 'Nelder-Mead'
self._align_minimizer_options: dict[str, Any] | None = None
self._align_tolerance = 1e-4 # Default tolerance value
# Mapping options
self._mapping_species: list[str] | None = None
# Validation options
self._min_atom_distance: float = 0.5
# Functions to be called during build() to create sites
self._site_generators: list[Callable] = []
return self
[docs]
def with_structure(self, structure) -> TrajectoryBuilder:
"""Set the structure to analyse.
Args:
structure: A pymatgen Structure object
Returns:
self: For method chaining
"""
self._structure = structure
return self
[docs]
def with_reference_structure(self, reference_structure) -> TrajectoryBuilder:
"""Set the reference structure for complex site types.
Args:
reference_structure: A pymatgen Structure object representing
the ideal reference structure
Returns:
self: For method chaining
"""
self._reference_structure = reference_structure
return self
[docs]
def with_mobile_species(self, species: str | list[str]) -> TrajectoryBuilder:
"""Set the mobile species to track.
Args:
species: Species string or list of species strings for mobile atoms
Returns:
self: For method chaining
"""
self._mobile_species = species
return self
[docs]
def with_structure_alignment(self,
align: bool = True,
align_species: str | list[str] | None = None,
align_metric: str = 'rmsd',
align_algorithm: str = 'Nelder-Mead',
align_minimizer_options: dict[str, Any] | None = None,
align_tolerance: float = 1e-4) -> 'TrajectoryBuilder':
"""Set options for aligning reference and target structures.
Structure alignment finds the optimal translation vector to superimpose
the reference structure onto the target structure, minimizing distances
between corresponding atoms.
Note:
Structure alignment is ENABLED by default when using polyhedral or dynamic
Voronoi sites, even if this method is not explicitly called. To disable
alignment, call this method with align=False.
All parameters are optional and have sensible defaults:
Args:
align: Whether to perform structure alignment. Default is True.
align_species: Species to use for alignment. Can be a string or list of strings.
Default is None, which means:
- If mapping species have been set with with_site_mapping(), those species will be used
- Otherwise, all common species between structures will be used
align_metric: Metric for alignment. Options are:
- 'rmsd': Root-mean-square deviation (default)
- 'max_dist': Maximum distance between any atom pair
align_algorithm: Algorithm for optimization. Options are:
- 'Nelder-Mead': Local optimizer, faster but may find local minima (default)
- 'differential_evolution': Global optimizer, more robust but slower
align_minimizer_options: Additional options for the minimizer as a dictionary.
Default is None (use algorithm defaults).
align_tolerance: Convergence tolerance for alignment optimizer. Default is 1e-4.
Lower values (e.g., 1e-5) give more precise alignment but may take longer.
Returns:
self: For method chaining
Example::
# Use default alignment (enabled, all species)
builder.with_reference_structure(reference)
builder.with_polyhedral_sites(...)
# Specify alignment species explicitly
builder.with_structure_alignment(align_species=["O", "Ti"])
# Disable alignment
builder.with_structure_alignment(align=False)
# Use global optimization for challenging alignments
builder.with_structure_alignment(
align_algorithm='differential_evolution',
align_minimizer_options={'popsize': 20}
)
"""
self._align = align
# Convert single species string to a list
if isinstance(align_species, str):
self._align_species = [align_species]
else:
self._align_species = align_species
self._align_metric = align_metric
self._align_algorithm = align_algorithm
self._align_minimizer_options = align_minimizer_options
self._align_tolerance = align_tolerance
return self
[docs]
def with_site_mapping(self, mapping_species: str | list[str] | None) -> TrajectoryBuilder:
"""Set the species to use for mapping sites between reference and target structures.
Site mapping identifies corresponding sites between structures even when
atom counts differ, for example when structures have different numbers
of mobile ions but the same framework atoms.
If mapping species are specified but alignment species are not:
- The mapping species will also be used for alignment (unless alignment is disabled)
If mapping species are not specified:
- The alignment species will be used for mapping
Args:
mapping_species: Species to use for mapping. Can be a string or list of strings.
If None, alignment species will be used for mapping.
Returns:
self: For method chaining
"""
# Convert single species string to a list
if isinstance(mapping_species, str):
self._mapping_species = [mapping_species]
else:
self._mapping_species = mapping_species
return self
[docs]
def with_min_atom_distance(self, distance: float) -> 'TrajectoryBuilder':
"""Set the minimum allowed distance between same-species atoms
in the reference structure.
If any pair of atoms of the same species in the reference
structure is closer than this threshold, ``build()`` raises
``ValueError``. This catches reference structures where atoms
sit on a general Wyckoff position instead of the correct
special position, producing duplicate coordination environments.
This check runs whenever a reference structure is set,
regardless of site type. It is most relevant for polyhedral
and dynamic Voronoi site workflows, but also applies when a
reference structure is provided for mapping or alignment.
Set to 0 to disable the check.
Args:
distance: Minimum distance in the same units as the
lattice parameters. Must be non-negative.
The builder default is 0.5.
Returns:
self: For method chaining.
Raises:
ValueError: If distance is negative.
"""
if distance < 0:
raise ValueError(
f"min_atom_distance must be non-negative, got {distance}"
)
self._min_atom_distance = distance
return self
[docs]
def with_spherical_sites(self,
centres: list[list[float]],
radii: float | list[float],
labels: str | list[str] | None = None) -> TrajectoryBuilder:
"""Define spherical sites.
Note: Sites will be generated when build() is called.
Args:
centres: list of fractional coordinate centres for spherical sites
radii: either a single radius (float) to use for all sites, or a list
of radii (one per centre)
labels: either a single label (str) to use for all sites, a list of
labels (one per centre), or None
Returns:
self: For method chaining
"""
# Convert single radius to list if needed
if isinstance(radii, (float, int)):
radii = [radii] * len(centres)
# Convert single label to list if needed
if isinstance(labels, str):
labels = [labels] * len(centres)
# Validate lengths
if len(centres) != len(radii):
raise ValueError("Number of centres must match number of radii")
if labels is not None and len(centres) != len(labels):
raise ValueError("Number of centres must match number of labels")
# Define the site generation function but don't execute it yet
def create_spherical_sites() -> Sequence[SphericalSite]:
# Create spherical sites
sites = []
for i, (centre, radius) in enumerate(zip(centres, radii)):
label = labels[i] if labels and i < len(labels) else None
sites.append(SphericalSite(
frac_coords=np.array(centre),
rcut=radius,
label=label
))
return sites
# Store the function for later execution
self._site_generators.append(create_spherical_sites)
return self
[docs]
def with_voronoi_sites(self,
centres: list[list[float]],
labels: list[str] | None = None) -> TrajectoryBuilder:
"""Define Voronoi sites.
Note: Sites will be generated when build() is called.
"""
# Define the site generation function but don't execute it yet
def create_voronoi_sites() -> Sequence[VoronoiSite]:
# Create Voronoi sites
sites = []
for i, centre in enumerate(centres):
label = labels[i] if labels and i < len(labels) else None
sites.append(VoronoiSite(
frac_coords=np.array(centre),
label=label
))
return sites
# Store the function for later execution
self._site_generators.append(create_voronoi_sites)
return self
[docs]
def with_polyhedral_sites(self,
centre_species: str,
vertex_species: str | list[str],
cutoff: float,
n_vertices: int,
label: str | None = None,
use_reference_centers: bool = True) -> TrajectoryBuilder:
"""Define polyhedral sites using the ReferenceBasedSites workflow.
Creates polyhedral sites by identifying coordination environments in the reference
structure and mapping them to corresponding sites in the target structure. Each site
is defined by a polyhedron formed by vertex atoms around a central atom.
The workflow involves:
1. Finding coordination environments in the reference structure
2. Mapping these environments to the target structure
3. Creating PolyhedralSite objects with proper periodic boundary handling
Note:
Sites will be generated when build() is called, not immediately.
Requires both structure and reference_structure to be set.
Args:
centre_species: Atomic species at the centre of coordination environments.
vertex_species: Atomic species at vertices of coordination polyhedra.
Can be a single species string or list of species strings.
cutoff: Maximum distance (in Ångströms) for vertex atoms to be considered
part of the coordination environment.
n_vertices: Number of vertex atoms required for each polyhedral site.
label: Optional label to assign to all created sites. Default is None.
use_reference_centers: Controls periodic boundary condition handling for
reference-based sites (polyhedral and dynamic Voronoi sites).
Default is True.
* ``True`` (recommended) -- Reference-based PBC correction. Defines
a reference center for each site and unwraps vertex coordinates
relative to this center. Correctly handles sites that naturally
span >50% of unit cell dimensions, even in small simulation cells.
* ``False`` (advanced usage) -- Spread-based PBC correction. If
vertex coordinates span >50% of the unit cell in any dimension,
assumes this indicates PBC wrapping and shifts coordinates
accordingly. WARNING: Gives incorrect results when sites
legitimately span >50% of the unit cell (e.g., octahedral sites
in a 2x2x2 FCC supercell). May offer performance benefits for
some setups. Only use after verifying it works correctly for your
structures.
Returns:
self: For method chaining
Raises:
ValueError: At build() time if no coordination environments are found,
or if required structures are not set.
Example::
# Tetrahedral sites around Li atoms
builder.with_polyhedral_sites(
centre_species="Li",
vertex_species="O",
cutoff=2.5,
n_vertices=4,
label="tetrahedral"
)
# Octahedral sites with mixed vertex species
builder.with_polyhedral_sites(
centre_species="Ti",
vertex_species=["O", "F"],
cutoff=2.8,
n_vertices=6,
label="octahedral"
)
"""
# Define the site generation function but don't execute it yet
def create_polyhedral_sites() -> Sequence[PolyhedralSite]:
if not self._structure or not self._reference_structure:
raise ValueError("Both structure and reference_structure must be set for polyhedral sites")
# If alignment is enabled but no alignment species are specified,
# use mapping species for alignment if available
align_species = self._align_species
if self._align and align_species is None and self._mapping_species is not None:
align_species = self._mapping_species
# Create ReferenceBasedSites
rbs = ReferenceBasedSites(
reference_structure=self._reference_structure,
target_structure=self._structure,
align=self._align,
align_species=align_species,
align_metric=self._align_metric,
align_algorithm=self._align_algorithm,
align_minimizer_options=self._align_minimizer_options,
align_tolerance=self._align_tolerance # Pass the tolerance
)
# Determine mapping species (use alignment species if mapping species not specified)
target_species = self._mapping_species if self._mapping_species is not None else self._align_species
# Create sites
sites = rbs.create_polyhedral_sites(
center_species=centre_species,
vertex_species=vertex_species,
cutoff=cutoff,
n_vertices=n_vertices,
label=label,
target_species=target_species,
use_reference_centers=use_reference_centers
)
# Check if any sites were found
if not sites:
raise ValueError(
f"No polyhedral sites found for centre_species='{centre_species}', "
f"vertex_species='{vertex_species}', cutoff={cutoff}, n_vertices={n_vertices}. "
f"Try adjusting these parameters or verify that the specified species exist "
f"in the structure."
)
return sites
# Store the function for later execution
self._site_generators.append(create_polyhedral_sites)
return self
[docs]
def with_dynamic_voronoi_sites(self,
centre_species: str,
reference_species: str | list[str],
cutoff: float,
n_reference: int,
label: str | None = None,
use_reference_centers: bool = True) -> TrajectoryBuilder:
"""Define dynamic Voronoi sites using the ReferenceBasedSites workflow.
Creates dynamic Voronoi sites where the site centres are dynamically calculated
from the positions of reference atoms. Unlike fixed Voronoi sites, these adapt
to structural changes as the reference atoms move.
The workflow involves:
1. Finding coordination environments in the reference structure
2. Mapping these environments to the target structure
3. Creating DynamicVoronoiSite objects that calculate centres from reference atoms
Note:
Sites will be generated when build() is called, not immediately.
Requires both structure and reference_structure to be set.
Args:
centre_species: Atomic species at centres where sites will be located.
reference_species: Atomic species used as reference atoms to define
dynamic site centres. Can be a single species string or list of species strings.
cutoff: Maximum distance (in Ångströms) for reference atoms to be considered
part of the coordination environment.
n_reference: Number of reference atoms required for each dynamic site.
label: Optional label to assign to all created sites. Default is None.
use_reference_centers: Controls periodic boundary condition handling for
reference-based sites (polyhedral and dynamic Voronoi sites).
Default is True.
* ``True`` (recommended) -- Reference-based PBC correction. Defines
a reference center for each site and unwraps vertex coordinates
relative to this center. Correctly handles sites that naturally
span >50% of unit cell dimensions, even in small simulation cells.
* ``False`` (advanced usage) -- Spread-based PBC correction. If
vertex coordinates span >50% of the unit cell in any dimension,
assumes this indicates PBC wrapping and shifts coordinates
accordingly. WARNING: Gives incorrect results when sites
legitimately span >50% of the unit cell (e.g., octahedral sites
in a 2x2x2 FCC supercell). May offer performance benefits for
some setups. Only use after verifying it works correctly for your
structures.
Returns:
self: For method chaining
Raises:
ValueError: At build() time if no coordination environments are found,
or if required structures are not set.
Example::
# Dynamic sites at Li positions defined by neighbouring O atoms
builder.with_dynamic_voronoi_sites(
centre_species="Li",
reference_species="O",
cutoff=3.0,
n_reference=4,
label="tetrahedral_dynamic"
)
# Sites with mixed reference species
builder.with_dynamic_voronoi_sites(
centre_species="Na",
reference_species=["O", "F"],
cutoff=2.8,
n_reference=6,
label="mixed_coordination"
)
"""
# Define the site generation function but don't execute it yet
def create_dynamic_voronoi_sites() -> Sequence[DynamicVoronoiSite]:
if not self._structure or not self._reference_structure:
raise ValueError("Both structure and reference_structure must be set for dynamic Voronoi sites")
# If alignment is enabled but no alignment species are specified,
# use mapping species for alignment if available
align_species = self._align_species
if self._align and align_species is None and self._mapping_species is not None:
align_species = self._mapping_species
# Create ReferenceBasedSites
rbs = ReferenceBasedSites(
reference_structure=self._reference_structure,
target_structure=self._structure,
align=self._align,
align_species=align_species,
align_metric=self._align_metric,
align_algorithm=self._align_algorithm,
align_minimizer_options=self._align_minimizer_options,
align_tolerance=self._align_tolerance
)
# Determine mapping species (use alignment species if mapping species not specified)
target_species = self._mapping_species if self._mapping_species is not None else self._align_species
# Create sites
sites = rbs.create_dynamic_voronoi_sites(
center_species=centre_species,
reference_species=reference_species,
cutoff=cutoff,
n_reference=n_reference,
label=label,
target_species=target_species,
use_reference_centers=use_reference_centers
)
# Check if any sites were found
if not sites:
raise ValueError(
f"No dynamic Voronoi sites found for centre_species='{centre_species}', "
f"reference_species='{reference_species}', cutoff={cutoff}, n_reference={n_reference}. "
f"Try adjusting these parameters or verify that the specified species exist "
f"in the structure."
)
return sites
# Store the function for later execution
self._site_generators.append(create_dynamic_voronoi_sites)
return self
[docs]
def with_existing_sites(self, sites: list) -> TrajectoryBuilder:
"""Use existing site objects."""
# Define a simple function that returns the provided sites
def return_existing_sites() -> list[Site]:
return sites
# Store the function for later execution
self._site_generators.append(return_existing_sites)
return self
[docs]
def with_existing_atoms(self, atoms: list) -> TrajectoryBuilder:
"""Use existing atom objects.
Args:
atoms: list of atom objects
Returns:
self: For method chaining
"""
self._atoms = atoms
return self
def _validate_reference_atom_distances(self) -> None:
"""Check that no same-species atom pairs in the reference
structure are closer than ``_min_atom_distance``."""
from site_analysis.distances import all_mic_distances
from site_analysis.tools import indices_for_species
ref = self._reference_structure
lattice_matrix = np.array(ref.lattice.matrix) # type: ignore[union-attr]
species_list = [s.species_string for s in ref] # type: ignore[union-attr]
frac_coords = np.array(ref.frac_coords) # type: ignore[union-attr]
for sp in set(species_list):
indices = indices_for_species(species_list, sp)
if len(indices) < 2:
continue
coords = frac_coords[indices]
dists = all_mic_distances(coords, coords, lattice_matrix)
np.fill_diagonal(dists, np.inf)
min_dist = float(np.min(dists))
if min_dist < self._min_atom_distance:
i_local, j_local = np.unravel_index(
int(np.argmin(dists)), dists.shape)
raise ValueError(
f"Reference structure has {sp} atoms at indices "
f"{indices[i_local]} and {indices[j_local]} that are "
f"only {min_dist:.3f} apart (threshold: "
f"{self._min_atom_distance}). This typically means "
f"the atom coordinate is on a general Wyckoff position "
f"instead of the correct special position, which "
f"produces duplicate coordination environments. "
f"To disable this check, call "
f".with_min_atom_distance(0) on the builder."
)
def _validate_unique_sites(self, sites: list[Site]) -> None:
"""Check that no two sites share the same defining indices.
Applies to PolyhedralSite (vertex_indices) and
DynamicVoronoiSite (reference_indices). Skipped for
VoronoiSite and SphericalSite where duplicate detection
based on coordinates is unreliable.
"""
from site_analysis.polyhedral_site import PolyhedralSite
from site_analysis.dynamic_voronoi_site import DynamicVoronoiSite
from site_analysis.voronoi_site import VoronoiSite
from site_analysis.spherical_site import SphericalSite
if not sites:
return
if isinstance(sites[0], PolyhedralSite):
index_attr = "vertex_indices"
elif isinstance(sites[0], DynamicVoronoiSite):
index_attr = "reference_indices"
elif isinstance(sites[0], (VoronoiSite, SphericalSite)):
return
else:
return # Unknown site type — skip gracefully
seen: dict[frozenset[int], int] = {}
for site in sites:
key = frozenset(getattr(site, index_attr))
if key in seen:
raise ValueError(
f"Duplicate sites: site {seen[key]} and site "
f"{site.index} share the same {index_attr} "
f"{sorted(key)}. This typically means the reference "
f"structure has multiple atoms inside the same "
f"coordination environment."
)
seen[key] = site.index
[docs]
def build(self) -> Trajectory:
"""Build and return the Trajectory object.
This method validates all required parameters and generates sites
using the previously configured site generator.
Returns:
The constructed Trajectory object.
Raises:
ValueError: If required parameters are missing, if the
reference structure has same-species atom pairs closer
than ``min_atom_distance``, or if duplicate sites are
detected.
"""
# Validate basic requirements
if not self._structure:
raise ValueError("Structure must be set")
if not self._site_generators:
raise ValueError("Site type must be defined using one of the with_*_sites methods")
# Pre-build: validate reference structure
if self._reference_structure is not None and self._min_atom_distance > 0:
self._validate_reference_atom_distances()
# Reset the site index counter
Site.reset_index()
# Generate all sites
sites: list[Site] = []
site_type = None
for generator in self._site_generators:
generated_sites = generator()
# Verify site type consistency
if generated_sites:
current_type = type(generated_sites[0])
if site_type is None:
site_type = current_type
elif site_type != current_type:
raise TypeError(f"Cannot mix site types: {site_type.__name__} and {current_type.__name__}")
sites.extend(generated_sites)
# Post-build: check for duplicate sites
self._validate_unique_sites(sites)
# Create atoms if not already set
if not self._atoms:
if not self._mobile_species:
raise ValueError("Mobile species must be set")
self._atoms = atoms_from_structure(self._structure, self._mobile_species)
if not self._atoms:
available_species = sorted(set(str(site.specie) for site in self._structure))
raise ValueError(
f"No atoms found matching mobile species '{self._mobile_species}'. "
f"Available species in structure: {available_species}"
)
# Create trajectory
trajectory = Trajectory(sites=sites, atoms=self._atoms)
# Reset the builder state for future use
self.reset()
return trajectory
[docs]
def create_trajectory_with_spherical_sites(
structure: Structure,
mobile_species: str | list[str],
centres: list[list[float]],
radii: float | list[float],
labels: str | list[str] | None = None
) -> Trajectory:
"""Create a Trajectory with spherical sites for site analysis.
Creates a trajectory object configured with spherical sites defined by centres
and radii. Each spherical site represents a spherical volume in the crystal
structure where atoms can be assigned based on distance from the centre.
Args:
structure: Pymatgen Structure object containing the crystal structure to analyse.
mobile_species: Species string (e.g., "Li") or list of species strings
(e.g., ["Li", "Na"]) identifying the mobile atoms to track.
centres: List of fractional coordinate triplets defining the centres of
spherical sites. Each centre should be a list of three floats [x, y, z].
radii: Cutoff radius in Ångströms for spherical sites. If a single float
is provided, all sites use the same radius. If a list is provided,
it must have the same length as centres.
labels: Optional labels for the sites. Can be a single string applied to
all sites, a list of strings (one per site), or None for no labels.
Returns:
Trajectory: Configured trajectory object ready for site analysis.
Raises:
ValueError: If the number of centres doesn't match the number of radii
or labels when lists are provided.
"""
builder = TrajectoryBuilder()
return (builder
.with_structure(structure)
.with_mobile_species(mobile_species)
.with_spherical_sites(centres, radii, labels)
.build())
[docs]
def create_trajectory_with_voronoi_sites(
structure: Structure,
mobile_species: str | list[str],
centres: list[list[float]],
labels: list[str] | None = None
) -> Trajectory:
"""Create a Trajectory with Voronoi sites for site analysis.
Creates a trajectory object configured with Voronoi sites that partition space
based on proximity to site centres. Each point in space is assigned to the
Voronoi site with the nearest centre, ensuring complete space-filling coverage.
Args:
structure: Pymatgen Structure object containing the crystal structure to analyse.
mobile_species: Species string (e.g., "Li") or list of species strings
(e.g., ["Li", "Na"]) identifying the mobile atoms to track.
centres: List of fractional coordinate triplets defining the centres of
Voronoi sites. Each centre should be a list of three floats [x, y, z].
labels: Optional list of string labels for the sites. Must have the same
length as centres if provided, or None for no labels.
Returns:
Trajectory: Configured trajectory object ready for site analysis.
Raises:
ValueError: If the number of labels doesn't match the number of centres.
"""
builder = TrajectoryBuilder()
return (builder
.with_structure(structure)
.with_mobile_species(mobile_species)
.with_voronoi_sites(centres, labels)
.build())
[docs]
def create_trajectory_with_polyhedral_sites(
structure: Structure,
reference_structure: Structure,
mobile_species: str | list[str],
centre_species: str,
vertex_species: str | list[str],
cutoff: float,
n_vertices: int,
label: str | None = None,
align: bool = True,
align_species: str | list[str] | None = None,
align_metric: str = 'rmsd',
align_algorithm: str = 'Nelder-Mead',
align_minimizer_options: dict[str, Any] | None = None,
mapping_species: str | list[str] | None = None,
align_tolerance: float = 1e-4,
use_reference_centers: bool = True
) -> Trajectory:
"""Create a Trajectory with polyhedral sites using reference-based workflow.
Creates a trajectory object with polyhedral sites by identifying coordination
environments in a reference structure and mapping them to corresponding sites
in the target structure. Each site is defined by a polyhedron formed by vertex
atoms around a central atom.
Args:
structure: Pymatgen Structure object containing the target structure to analyse.
reference_structure: Pymatgen Structure object defining ideal coordination
environments to identify and map.
mobile_species: Species string (e.g., "Li") or list of species strings
identifying the mobile atoms to track.
centre_species: Atomic species at the centre of coordination environments
in the reference structure.
vertex_species: Atomic species at vertices of coordination polyhedra.
Can be a single species string or list of species strings.
cutoff: Maximum distance in Ångströms for vertex atoms to be considered
part of the coordination environment.
n_vertices: Number of vertex atoms required for each polyhedral site.
label: Optional string label to assign to all created sites.
align: Whether to perform structure alignment before site mapping.
align_species: Species to use for structure alignment. If None and
mapping_species is specified, mapping_species will be used.
align_metric: Alignment metric. Options are 'rmsd' for root-mean-square
deviation or 'max_dist' for maximum distance.
align_algorithm: Optimisation algorithm. Options are 'Nelder-Mead' for
local optimisation or 'differential_evolution' for global optimisation.
align_minimizer_options: Additional options dictionary for the alignment
optimiser.
mapping_species: Species to use for mapping sites between structures.
If None, align_species will be used.
align_tolerance: Convergence tolerance for alignment optimiser.
use_reference_centers: Whether to use reference centers for
PBC handling. See TrajectoryBuilder.with_polyhedral_sites() for details.
Default is True.
Returns:
Trajectory: Configured trajectory object ready for site analysis.
Raises:
ValueError: If no coordination environments are found, if required
structures are not set, or if structure alignment fails.
"""
builder = TrajectoryBuilder()
return (builder
.with_structure(structure)
.with_reference_structure(reference_structure)
.with_mobile_species(mobile_species)
.with_structure_alignment(
align=align,
align_species=align_species,
align_metric=align_metric,
align_algorithm=align_algorithm,
align_minimizer_options=align_minimizer_options,
align_tolerance=align_tolerance
)
.with_site_mapping(mapping_species)
.with_polyhedral_sites(
centre_species,
vertex_species,
cutoff,
n_vertices,
label,
use_reference_centers)
.build())
[docs]
def create_trajectory_with_dynamic_voronoi_sites(
structure: Structure,
reference_structure: Structure,
mobile_species: str | list[str],
centre_species: str,
reference_species: str | list[str],
cutoff: float,
n_reference: int,
label: str | None = None,
align: bool = True,
align_species: str | list[str] | None = None,
align_metric: str = 'rmsd',
align_algorithm: str = 'Nelder-Mead',
align_minimizer_options: dict[str, Any] | None = None,
mapping_species: str | list[str] | None = None,
align_tolerance: float = 1e-4,
use_reference_centers: bool = True
) -> Trajectory:
"""Create a Trajectory with dynamic Voronoi sites using reference-based workflow.
Creates a trajectory object with dynamic Voronoi sites where site centres are
dynamically calculated from the positions of reference atoms. Unlike fixed
Voronoi sites, these adapt to structural changes as the reference atoms move,
making them suitable for analysing deformable frameworks.
Args:
structure: Pymatgen Structure object containing the target structure to analyse.
reference_structure: Pymatgen Structure object defining coordination
environments to identify and map.
mobile_species: Species string (e.g., "Li") or list of species strings
identifying the mobile atoms to track.
centre_species: Atomic species at centres where dynamic sites will be located
in the reference structure.
reference_species: Atomic species used as reference atoms to define dynamic
site centres. Can be a single species string or list of species strings.
cutoff: Maximum distance in Ångströms for reference atoms to be considered
part of the coordination environment.
n_reference: Number of reference atoms required for each dynamic site.
label: Optional string label to assign to all created sites.
align: Whether to perform structure alignment before site mapping.
align_species: Species to use for structure alignment. If None and
mapping_species is specified, mapping_species will be used.
align_metric: Alignment metric. Options are 'rmsd' for root-mean-square
deviation or 'max_dist' for maximum distance.
align_algorithm: Optimisation algorithm. Options are 'Nelder-Mead' for
local optimisation or 'differential_evolution' for global optimisation.
align_minimizer_options: Additional options dictionary for the alignment
optimiser.
mapping_species: Species to use for mapping sites between structures.
If None, align_species will be used.
align_tolerance: Convergence tolerance for alignment optimiser.
use_reference_centers: Whether to use reference centers for
PBC handling. See TrajectoryBuilder.with_polyhedral_sites() for details.
Default is True.
Returns:
Trajectory: Configured trajectory object ready for site analysis.
Raises:
ValueError: If no coordination environments are found, if required
structures are not set, or if structure alignment fails.
"""
builder = TrajectoryBuilder()
return (builder
.with_structure(structure)
.with_reference_structure(reference_structure)
.with_mobile_species(mobile_species)
.with_structure_alignment(
align=align,
align_species=align_species,
align_metric=align_metric,
align_algorithm=align_algorithm,
align_minimizer_options=align_minimizer_options,
align_tolerance=align_tolerance
)
.with_site_mapping(mapping_species)
.with_dynamic_voronoi_sites(
centre_species,
reference_species,
cutoff,
n_reference,
label,
use_reference_centers)
.build())