Source code for sknano.core.atoms.xyz_atoms

# -*- coding: utf-8 -*-
"""
===============================================================================
Atom classes with x, y, z attributes (:mod:`sknano.core.atoms.xyz_atoms`)
===============================================================================

.. currentmodule:: sknano.core.atoms.xyz_atoms

"""
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals

__docformat__ = 'restructuredtext en'

import numbers

from collections import OrderedDict
from math import fsum
from operator import attrgetter

import numpy as np

from sknano.core import rezero_array, xyz
from sknano.core.math import Vector, Vectors

from .atoms import Atom, Atoms

__all__ = ['XYZAtom', 'XYZAtoms']


[docs]class XYZAtom(Atom): """An `Atom` sub-class with x, y, z attributes. Parameters ---------- element : {str, int}, optional A string representation of the element symbol or an integer specifying an element atomic number. x, y, z : float, optional :math:`x, y, z` components of `XYZAtom` position vector relative to origin. """ def __init__(self, *args, x=None, y=None, z=None, **kwargs): super().__init__(*args, **kwargs) self._r0 = Vector([x, y, z]) self._r = Vector([x, y, z]) self.fmtstr = super().fmtstr + ", x={x:.6f}, y={y:.6f}, z={z:.6f}" def __eq__(self, other): if not self._is_valid_operand(other): return NotImplemented return self.r == other.r and super().__eq__(other) def __le__(self, other): if not self._is_valid_operand(other): return NotImplemented if self.r > other.r or not super().__le__(other): return False return True def __lt__(self, other): if not self._is_valid_operand(other): return NotImplemented if self.r >= other.r or not super().__lt__(other): return False return True def __ge__(self, other): if not self._is_valid_operand(other): return NotImplemented if self.r < other.r or not super().__ge__(other): return False return True def __gt__(self, other): if not self._is_valid_operand(other): return NotImplemented if self.r <= other.r or not super().__gt__(other): return False return True def __dir__(self): attrs = super().__dir__() attrs.extend(['x', 'y', 'z']) return attrs @property def x(self): """:math:`x`-coordinate in units of **Angstroms**. Returns ------- float :math:`x`-coordinate in units of **Angstroms**. """ return self._r.x @x.setter def x(self, value): """Set `Atom` :math:`x`-coordinate in units of **Angstroms**. Parameters ---------- value : float :math:`x`-coordinate in units of **Angstroms**. """ if not isinstance(value, numbers.Number): raise TypeError('Expected a number') self._r.x = value @property def y(self): """:math:`y`-coordinate in units of **Angstroms**. Returns ------- float :math:`y`-coordinate in units of **Angstroms**. """ return self._r.y @y.setter def y(self, value): """Set `Atom` :math:`y`-coordinate in units of **Angstroms**. Parameters ---------- value : float :math:`y`-coordinate in units of **Angstroms**. """ if not isinstance(value, numbers.Number): raise TypeError('Expected a number') self._r.y = value @property def z(self): """:math:`z`-coordinate in units of **Angstroms**. Returns ------- float :math:`z`-coordinate in units of **Angstroms**. """ return self._r.z @z.setter def z(self, value): """Set `Atom` :math:`z`-coordinate in units of **Angstroms**. Parameters ---------- value : float :math:`z`-coordinate in units of **Angstroms**. """ if not isinstance(value, numbers.Number): raise TypeError('Expected a number') self._r.z = value @property def r(self): """:math:`x, y, z` components of `Atom` position vector. Returns ------- :class:`Vector` 3D :class:`Vector` of [:math:`x, y, z`] coordinates of `Atom`. """ return self._r @r.setter def r(self, value): """Set :math:`x, y, z` components of `Atom` position vector. Parameters ---------- value : array_like :math:`x, y, z` coordinates of `Atom` position vector relative to the origin. """ if not isinstance(value, (list, np.ndarray)): raise TypeError('Expected an array_like object') self._r[:] = Vector(value, nd=3) @property def r0(self): """:math:`x, y, z` components of `Atom` position vector at t=0. Returns ------- ndarray 3-element ndarray of [:math:`x, y, z`] coordinates of `Atom`. """ return self._r0 @r0.setter def r0(self, value): """Set :math:`x, y, z` components of `Atom` position vector at t=0. Parameters ---------- value : array_like :math:`x, y, z` coordinates of `Atom` position vector relative to the origin. """ if not isinstance(value, (list, np.ndarray)): raise TypeError('Expected an array_like object') self._r0[:] = Vector(value, nd=3) @property def dr(self): """:math:`x, y, z` components of `Atom` displacement vector. Returns ------- ndarray 3-element ndarray of [:math:`x, y, z`] coordinates of `Atom`. """ return self.r - self.r0
[docs] def get_coords(self, asdict=False): """Return atom coords. Parameters ---------- asdict : bool, optional Returns ------- coords : :class:`~python:collections.OrderedDict` or ndarray """ if asdict: return OrderedDict(list(zip(xyz, self.r))) else: return self.r
[docs] def rezero(self, epsilon=1.0e-10): """Alias for :meth:`Atom.rezero_xyz`, but calls `super` class \ `rezero` method as well.""" self.r.rezero(epsilon) super().rezero(epsilon)
[docs] def rezero_coords(self, epsilon=1.0e-10): """Alias for :meth:`Atom.rezero_xyz`.""" self.rezero_xyz(epsilon=epsilon)
[docs] def rezero_xyz(self, epsilon=1.0e-10): """Re-zero position vector components. Set position vector components with absolute value less than `epsilon` to zero. Unlike the :meth:`Atom.rezero` method, this method does **not** call the super class `rezero` method. Parameters ---------- epsilon : float smallest allowed absolute value of any :math:`x,y,z` component. """ self.r.rezero(epsilon=epsilon)
[docs] def todict(self): """Return :class:`~python:dict` of constructor parameters.""" super_dict = super().todict() super_dict.update(dict(x=self.x, y=self.y, z=self.z)) return super_dict
[docs]class XYZAtoms(Atoms): """An `Atoms` sub-class for `XYZAtom`\ s. A container class for :class:`~sknano.core.atoms.XYZAtom` objects. Parameters ---------- atoms : {None, sequence, `XYZAtoms`}, optional if not `None`, then a list of `XYZAtom` instance objects or an existing `XYZAtoms` instance object. """ @property def __atom_class__(self): return XYZAtom def sort(self, key=attrgetter('r'), reverse=False): super().sort(key=key, reverse=reverse) @property def center_of_mass(self): """Center-of-Mass coordinates of `Atoms`. Computes the position vector of the center-of-mass coordinates: .. math:: \\mathbf{R}_{COM} = \\frac{1}{M}\\sum_{i=1}^{N_{\\mathrm{atoms}}} m_i\\mathbf{r}_i Returns ------- com : :class:`~sknano.core.math.Vector` The position vector of the center of mass coordinates. """ masses = np.asarray([self.masses]) coords = self.coords MxR = masses.T * coords com = Vector(np.sum(MxR, axis=0) / np.sum(masses)) com.rezero() return com @property def com(self): """Alias for :attr:`~XYZAtoms.center_of_mass`.""" return self.center_of_mass @property def CM(self): """Alias for :attr:`~XYZAtoms.center_of_mass`.""" return self.center_of_mass @property def centroid(self): """Centroid of `Atoms`. Computes the position vector of the centroid of the `Atoms` coordinates. .. math:: \\mathbf{C} = \\frac{\\sum_{i=1}^{N_{\\mathrm{atoms}}} \\mathbf{r}_i}{N_{\\mathrm{atoms}}} Returns ------- C : :class:`~sknano.core.math.Vector` The position vector of the centroid coordinates. """ C = Vector(np.mean(self.coords, axis=0)) C.rezero() return C @property def coords(self): """Alias for :attr:`Atoms.r`.""" return self.r @property def positions(self): """Alias for :attr:`Atoms.r`.""" return self.r @property def r(self): """:class:`Vectors` of :attr:`Atom.r` position `Vector`\ s""" return Vectors([atom.r for atom in self]) @property def dr(self): """:class:`Vectors` of :attr:`Atom.dr` displacement `Vector`\ s""" return Vectors([atom.dr for atom in self]) @property def x(self): """:class:`~numpy:numpy.ndarray` of :attr:`XYZAtom.x` coordinates.""" # return np.asarray(self.r)[:, 0] return self.r.x @property def y(self): """:class:`~numpy:numpy.ndarray` of :attr:`XYZAtom.y` coordinates.""" # return np.asarray(self.r)[:, 1] return self.r.y @property def z(self): """:class:`~numpy:numpy.ndarray` of :attr:`XYZAtom.z` coordinates.""" # return np.asarray(self.r)[:, 2] return self.r.z @property def inertia_tensor(self): """Return inertia tensor about the origin.""" masses = self.masses r = self.r - self.center_of_mass x, y, z = r.x, r.y, r.z Ixx = fsum(masses * (y**2 + z**2)) Iyy = fsum(masses * (x**2 + z**2)) Izz = fsum(masses * (x**2 + y**2)) Ixy = Iyx = fsum(-masses * x * y) Ixz = Izx = fsum(-masses * x * z) Iyz = Izy = fsum(-masses * y * z) I = np.array([[Ixx, Ixy, Ixz], [Iyx, Iyy, Iyz], [Izx, Izy, Izz]]) return rezero_array(I, epsilon=1e-10) @property def moment_of_inertia(self): """Alias for :attr:`~XYZAtoms.inertia_tensor`.""" return self.inertia_tensor @property def principal_axes(self): """Return principal axes of rotation computed from the inertia tensor. Since the :attr:`~XYZAtoms.inertia_tensor` forms a :math:`3\\times3` real symmetric matrix :math:`[I]`, then there is a matrix :math:`[P]=[\\mathbf{p}_1\\,\\mathbf{p}_2\\,\\mathbf{p}_3]` that orthogonally diagonalizes :math:`[I]` such that :math:`[\\Lambda]=[P]^T [I] [P]` is a diagonal matrix: .. math:: [\\Lambda] = \\begin{bmatrix} \\lambda_1 & 0 & 0\\\\ 0 & \\lambda_2 & 0\\\\ 0 & 0 & \\lambda_3 \\end{bmatrix} where :math:`\\lambda_1,\\lambda_2,\\lambda_3` are the eigenvalues of :math:`[I]` corresponding to the eigenvectors :math:`\\mathbf{p}_1, \\mathbf{p}_2, \\mathbf{p}_3`, which form the column vectors of matrix :math:`[P]` and are the principal axes of the moment of inertia. In other words, the matrix :math:`[P]` is a rotation matrix which transforms the coordinates :math:`\\mathbf{r}` of the atoms such that the moment of inertia i Returns ------- p : :class:`~sknano.core.math.Vectors` :class:`~sknano.core.math.Vectors` object where `p[0]`, `p[1]`, and `p[2]` are the 3 principal axes corresponding to the eigenvectors that form the successive columns of :math:`P`. """ w, v = np.linalg.eig(self.inertia_tensor) v = rezero_array(v[:, np.argsort(w)[::-1]], epsilon=1e-10) # v = rezero_array(v, epsilon=1e-10) p = Vectors(list(map(Vector, [v[:, i] for i in range(3)]))) return p @property def principal_moments_of_inertia(self): """Return principal moments of inertia.""" w = np.linalg.eigvals(self.inertia_tensor) return w[np.argsort(w)[::-1]] @property def radius_of_gyration(self): """Return radius of gyration. .. math:: R_g = \\sqrt{\\frac{1}{M}\\sum_{i=1}^{N} m_i(\\mathbf{r}_i-\\mathbf{R})^2} """ r = self.r masses = self.masses R = self.center_of_mass M = self.M return np.sqrt(np.sum(masses * (r - R).dot(r - R)) / M) def align_principal_axis(self, index, vector): """Align :attr:`~XYZAtoms.principal_axes`[`index`] along `vector`. Parameters ---------- index : :class:`~python:int` vector : {:class:`~python:list`, :class:`~numpy:numpy.ndarray`} Examples -------- >>> from sknano.generators import SWNTGenerator >>> swnt = SWNTGenerator(10, 5) """ self.rotate(from_vector=self.principal_axes[index], to_vector=Vector(vector)) def center_center_of_mass(self, axis=None): """Center atoms on center-of-mass coordinates.""" self.translate(-self.center_of_mass) def center_com(self, axis=None): """Alias for :attr:`~XYZAtoms.center_center_of_mass`.""" self.center_center_of_mass(axis=axis) def center_CM(self, axis=None): """Alias for :attr:`~XYZAtoms.center_center_of_mass`.""" self.center_center_of_mass(axis=axis) def center_centroid(self): """Center :attr:`~XYZAtoms.centroid` on origin.""" self.translate(-self.centroid) def clip_bounds(self, region, center_before_clipping=False): """Remove atoms outside the given region. Parameters ---------- region : :class:`~sknano.core.geometric_regions.GeometricRegion` """ centroid0 = None if center_before_clipping: centroid0 = self.centroid self.translate(-centroid0) self.data = [atom for atom in self if region.contains(atom.r)] if centroid0 is not None: self.translate(centroid0) def get_coords(self, asdict=False): """Return atom coords. Parameters ---------- asdict : :class:`~python:bool`, optional Returns ------- coords : :class:`~python:collections.OrderedDict` or \ :class:`~numpy:numpy.ndarray` """ coords = self.coords if asdict: return OrderedDict(list(zip(xyz, coords.T))) else: return coords def rezero_coords(self, epsilon=1.0e-10): """Alias for :meth:`Atoms.rezero_xyz`.""" self.rezero(epsilon=epsilon) def rezero_xyz(self, epsilon=1.0e-10): """Rezero position vector components with absolute value less than \ `epsilon`. Calls the :meth:`XYZAtom.rezero_xyz` method on each `atom` in `self`. Parameters ---------- epsilon : :class:`~python:float` values with absolute value less than `epsilon` are set to zero. """ [atom.rezero_xyz(epsilon=epsilon) for atom in self]