Source code for viperleed.calc.classes.layer

"""Module layer of viperleed.calc.classes.

Classes storing position and atom list of a Layer, and its SubLayer
subclass. The latter is for atoms of the same chemical species at
the same z position.
"""

__authors__ = (
    'Florian Kraushofer (@fkraushofer)',
    'Michele Riva (@michele-riva)',
    )
__copyright__ = 'Copyright (c) 2019-2024 ViPErLEED developers'
__created__ = '2019-06-13'
__license__ = 'GPLv3+'

from viperleed.calc.classes.atom_containers import AtomContainer
from viperleed.calc.classes.atom_containers import AtomContainerError


class LayerError(AtomContainerError):
    """Base exception for Layer objects."""


class LayerHasNoAtomsError(LayerError):
    """Operation cannot be performed as the layer is empty."""


[docs]class Layer(AtomContainer): # TODO: modify description of .cartori when flipping .cartpos[2] -- Issue #174 """A container of atoms residing close to one another along z. This is intended to be used with Slab objects. Has origin, atoms (a subset of the ones in slab), and a number. Attributes ---------- atlist : list of Atom The atoms that belong to this layer. cartori : numpy.ndarray The Cartesian origin of this layer. It is the position where a plane passing through the topmost atom in this layer intersects the c vector of the slab's unit cell. Notice that `cartori` is in the same Cartesian frame as the atoms (i.e., z increases going from vacuum into the solid). A call to update_position() updates this attribute. cartbotz : float Z (i.e., out-of-plane) position of the bottom-most atom in the layer. A call to update_position() updates this attribute. is_bulk : bool Whether this layer has bulk character. num : int A progressive index (zero-based) identifying this layer within its slab. Normally, `layer.num == 0` for the layer closest to the solid/vacuum interface. slab : Slab The slab to which this layer belongs. """
[docs] def __init__(self, slab, num, is_bulk=False): """Initialize instance.""" self.slab = slab self.num = num self.is_bulk = is_bulk self.atlist = [] # Note about the next two: they should be kept in memory, # rather than re-determined on-the-fly, since the position # of the atoms in question may change during a search, but # TensErLEED expects atom positions to be referred to the # layer position as it was used in the reference calculation self.cartori = None self.cartbotz = None
def __contains__(self, value): """Return whether this layer contains an atom.""" return value in self.atlist def __iter__(self): """Return an iterator of Atoms in this Layer.""" return iter(self.atlist) @property def n_atoms(self): """Return the number of atoms in this layer.""" return len(self.atlist) @property def thickness(self): """Return the z thickness of this layer. Note ---- This is calculated from the stored positions of this layer, and may not be up to date if atoms in this layer have moved along z since the last call to update_position(). """ return abs(self.cartbotz - self.cartori[2]) def update_position(self): """Update the Cartesian position of this layer from its atoms.""" if not self.atlist: raise LayerHasNoAtomsError( f'{type(self).__name__} needs atoms to update_position()' ) sorted_atoms = sorted(self, key=lambda atom: atom.pos[2]) topat = sorted_atoms[-1] botat = sorted_atoms[0] self.cartbotz = botat.cartpos[2] self.cartori = topat.pos[2] * self.slab.c_vector # So far x and y are correct, but z is still in the wrong # TODO: this will not be necessary when we flip .cartpos[2] -- Issue #174 # direction and with origin as POSCAR. Take the z directly # from the highest atom self.cartori[2] = topat.cartpos[2]
class SubLayer(Layer): # TODO: modify description of .cartori when flipping .cartpos[2] -- Issue #174 """A Layer with the same chemical element and the same z position. Attributes ---------- atlist : list of Atom The atoms that belong to this sublayer. cartori : numpy.ndarray The Cartesian origin of this layer. It is the position where a plane passing through the topmost atom in this layer intersects the c vector of the slab's unit cell. Notice that `cartori` is in the same Cartesian frame as the atoms (i.e., z increases going from vacuum into the solid). A call to update_position() updates this attribute. cartbotz : float Z (i.e., out-of-plane) position of the bottom-most atom in the layer. A call to update_position() updates this attribute. cartpos : numpy.ndarray The Cartesian position of this sublayer. Notice that this is not an alias for `cartori`. The (x, y) components of the two are not the same: `cartori[:2]` is a point along the c vector of the unit cell, while `cartpos[:2]` may be a point inside (or outside if atoms are not collapsed) the unit cell. element : str The chemical element of the atoms of this sublayer. is_bulk : bool Whether this layer has bulk character. num : int A progressive index (zero-based) identifying this sublayer within its slab. Normally, `sublayer.num == 0` for the sublayer closest to the solid/vacuum interface. pos : numpy.ndarray The fractional position of this sublayer. This is the fractional position corresponding to `cartpos`, not the one corresponding to `cartori`. slab : Slab The slab to which this sublayer belongs. symposlist : list Candidate positions for rotation / mirror / glide planes. """ def __init__(self, slab, num, is_bulk=False): """Initialize instance.""" super().__init__(slab, num, is_bulk=is_bulk) self.symposlist = [] def __str__(self): """Return a string version of this sublayer.""" n_atoms = self.n_atoms if not n_atoms: atoms = '(empty)' elif n_atoms == 1: atoms = self.atlist[0] else: atoms = [f'{at}' for at in self] return f'Sublayer(num={self.num}, {atoms}, cartbotz={self.cartbotz})' @property def element(self): """Return the chemical element for this sublayer.""" if not self.atlist: raise LayerHasNoAtomsError( f'A {type(self).__name__} without atoms has no element' ) return self.atlist[0].el @property def cartpos(self): """Return the Cartesian position of this sublayer.""" if not self.atlist: raise LayerHasNoAtomsError( f'A {type(self).__name__} without atoms has no cartpos' ) return self.atlist[0].cartpos @property def pos(self): """Return the fractional position of this sublayer.""" if not self.atlist: raise LayerHasNoAtomsError( f'A {type(self).__name__} without atoms has no pos' ) return self.atlist[0].pos