"""
This module provides the Sublattice and Sublattices classes.
"""
from copy import deepcopy
from icet.core.structure import find_lattice_sites_by_positions
from collections.abc import Iterator
from ase import Atoms
import copy
from itertools import product
from typing import Any
from string import ascii_uppercase
import numpy as np
from icet.tools.geometry import chemical_symbols_to_numbers
from icet.input_output.repr_tools import html_representation, text_representation
[docs]
class Sublattice:
"""
This class stores and provides information about one sublattice, which is
a set of sites of a supercell that allow the same species.
A sublattice is specific to a supercell, since it holds site indices.
Note
----
As a user you will usually not interact directly with objects of this type.
Parameters
----------
chemical_symbols
Species allowed on this sublattice.
indices
Indices of the sites of the supercell that belong to this sublattice.
symbol
Letter that labels the sublattice, such as ``'A'`` or ``'B'``.
"""
def __init__(self,
chemical_symbols: list[str],
indices: list[int],
symbol: str) -> None:
self._chemical_symbols = chemical_symbols
self._indices = indices
self._symbol = symbol
self._numbers = chemical_symbols_to_numbers(chemical_symbols)
@property
def chemical_symbols(self) -> list[str]:
""" Species allowed on this sublattice (copy). """
return copy.deepcopy(self._chemical_symbols)
@property
def atomic_numbers(self) -> list[int]:
""" Atomic numbers of the species allowed on this sublattice (copy). """
return self._numbers.copy()
@property
def indices(self) -> list[int]:
""" Indices of the sites that belong to this sublattice (copy). """
return self._indices.copy()
@property
def symbol(self) -> str:
""" Letter that labels the sublattice, such as ``'A'`` or ``'B'``. """
return self._symbol
def _get_rows(self) -> list[tuple[str, Any]]:
"""Returns the label and value pairs from which the string and HTML
representations are built.
"""
return [('symbol', self.symbol),
('chemical symbols', list(self._chemical_symbols)),
('number of sites', len(self._indices)),
('active', len(self._chemical_symbols) > 1)]
def __str__(self) -> str:
""" String representation. """
return text_representation(title='Sublattice', rows=self._get_rows(),
width=48, label_width=20)
def _repr_html_(self) -> str:
""" HTML representation. Used, e.g., in jupyter notebooks. """
return html_representation(title='Sublattice', rows=self._get_rows())
def __repr__(self) -> str:
""" Representation. """
s = type(self).__name__ + '('
s += f'symbol={self.symbol!r}'
s += f', chemical_symbols={list(self._chemical_symbols)}'
s += f', number_of_sites={len(self._indices)}'
s += ')'
return s
[docs]
class Sublattices:
"""
This class stores and provides information about the sublattices of a
supercell.
Sites that allow the same set of species form one sublattice.
The sublattices are labeled with letters in the order of their species,
with the active sublattices, on which more than one species is allowed,
before the inactive ones.
The class behaves as a sequence of :class:`Sublattice` objects.
Note
----
As a user you will usually not interact directly with objects of this type.
Parameters
----------
allowed_species
Species allowed on each site of the primitive structure, such as the
chemical symbols of a cluster space.
primitive_structure
Primitive structure the allowed species refer to.
structure
Supercell the sublattices are based on.
fractional_position_tolerance
Tolerance applied when comparing positions in fractional coordinates.
"""
def __init__(self,
allowed_species: list[list[str]],
primitive_structure: Atoms,
structure: Atoms,
fractional_position_tolerance: float) -> None:
self._structure = structure
# sorted unique sites, this basically decides A, B, C... sublattices
active_lattices = sorted(set([tuple(sorted(symbols))
for symbols in allowed_species if len(symbols) > 1]))
inactive_lattices = sorted(
set([tuple(sorted(symbols)) for symbols in allowed_species if len(symbols) == 1]))
self._allowed_species = active_lattices + inactive_lattices
n = int(np.sqrt(len(self._allowed_species))) + 1
symbols = [''.join(p) for r in range(1, n+1) for p in product(ascii_uppercase, repeat=r)]
lattice_sites = find_lattice_sites_by_positions(
primitive_structure, positions=structure.positions,
fractional_position_tolerance=fractional_position_tolerance)
self._sublattices = []
sublattice_to_indices = [[] for _ in range(len(self._allowed_species))]
for index, lattice_site in enumerate(lattice_sites):
# Get allowed species on this site
species = allowed_species[lattice_site.index]
# Get what sublattice those species correspond to
sublattice = self._allowed_species.index(tuple(sorted(species)))
sublattice_to_indices[sublattice].append(index)
for symbol, species, indices in zip(symbols, self._allowed_species, sublattice_to_indices):
sublattice = Sublattice(chemical_symbols=species, indices=indices, symbol=symbol)
self._sublattices.append(sublattice)
# Map lattice index to sublattice index
self._index_to_sublattice = {}
for k, sublattice in enumerate(self):
for index in sublattice.indices:
self._index_to_sublattice[index] = k
def __getitem__(self, key: int) -> Sublattice:
""" Returns the sublattice with the given index. """
return self._sublattices[key]
def __len__(self) -> int:
""" Returns the number of sublattices. """
return len(self._sublattices)
def __iter__(self) -> Iterator[Sublattice]:
""" Iterates over the sublattices. """
yield from self._sublattices
#: Titles of the columns of the string and HTML representations.
_COLUMN_TITLES = ('Symbol', 'Chemical symbols', 'Sites', 'Active')
def _get_rows(self) -> list[tuple[str, str, str, str]]:
"""Returns one row per sublattice from which the string and HTML
representations are built.
"""
return [(sublattice.symbol,
', '.join(sublattice.chemical_symbols),
str(len(sublattice.indices)),
str(len(sublattice.chemical_symbols) > 1))
for sublattice in self]
def __str__(self) -> str:
"""String representation.
The columns are as wide as their widest entry, so that a sublattice
that allows many species does not push the table past its frame.
"""
rows = self._get_rows()
widths = [max(len(title), *(len(row[column]) for row in rows)) if rows else len(title)
for column, title in enumerate(self._COLUMN_TITLES)]
def format_row(cells):
return ' ' + ' | '.join(cell.ljust(width)
for cell, width in zip(cells, widths)).rstrip()
width = len(' ' + ' | '.join(' ' * w for w in widths))
s = [' Sublattices '.center(width, '=')]
s += [format_row(self._COLUMN_TITLES)]
s += [''.center(width, '-')]
s += [format_row(row) for row in rows]
s += [''.center(width, '=')]
return '\n'.join(s)
def _repr_html_(self) -> str:
""" HTML representation. Used, e.g., in jupyter notebooks. """
return html_representation(title='Sublattices', rows=self._get_rows(),
header=self._COLUMN_TITLES)
def __repr__(self) -> str:
""" Representation. """
s = type(self).__name__ + '('
s += f'symbols={[sublattice.symbol for sublattice in self]}'
s += f', allowed_species={[list(species) for species in self._allowed_species]}'
s += ')'
return s
[docs]
def get_sublattice_index_from_site_index(self, index: int) -> int:
"""
Returns the index of the sublattice a site belongs to.
Parameters
----------
index
Index of the site in the supercell.
"""
return self._index_to_sublattice[index]
@property
def allowed_species(self) -> list[list[str]]:
""" Species allowed on each sublattice, in the order of the sublattices (copy). """
return deepcopy(self._allowed_species)
[docs]
def get_sublattice_sites(self, index: int) -> list[int]:
"""
Returns the indices of the sites that belong to a sublattice.
Parameters
----------
index
Index of the sublattice.
"""
return self[index].indices
[docs]
def get_allowed_symbols_on_site(self, index: int) -> list[str]:
"""
Returns the chemical symbols of the species allowed on a site.
Parameters
----------
index
Index of the site in the supercell.
"""
return self[self._index_to_sublattice[index]].chemical_symbols
[docs]
def get_allowed_numbers_on_site(self, index: int) -> list[int]:
"""
Returns the atomic numbers of the species allowed on a site.
Parameters
----------
index
Index of the site in the supercell.
"""
return self[self._index_to_sublattice[index]].atomic_numbers
@property
def active_sublattices(self) -> list[Sublattice]:
""" Sublattices on which more than one species is allowed. """
return [sl for sl in self if len(sl.chemical_symbols) > 1]
@property
def inactive_sublattices(self) -> list[Sublattice]:
""" Sublattices on which only one species is allowed. """
return [sl for sl in self if len(sl.chemical_symbols) == 1]
[docs]
def assert_occupation_is_allowed(self, chemical_symbols: list[str]) -> None:
"""
Checks that an occupation of the supercell respects the sublattices.
Parameters
----------
chemical_symbols
Chemical symbol on each site of the supercell.
Raises
------
ValueError
If the number of symbols differs from the number of sites, or if a
site carries a species that is not allowed on its sublattice.
"""
if len(chemical_symbols) != len(self._structure):
raise ValueError(f'Length of input chemical symbols ({len(chemical_symbols)}) does not'
f' match length of supercell ({len(self._structure)}')
for sl in self:
for i in sl.indices:
if not chemical_symbols[i] in sl.chemical_symbols:
msg = ('Occupations of structure not compatible with the sublattice.'
' Site {} with occupation {} not allowed on sublattice {}'
.format(i, chemical_symbols[i], sl.chemical_symbols))
raise ValueError(msg)