# -*- coding: utf-8 -*-
"""
==============================================================================
Base Atom classes (:mod:`sknano.core.atoms.atoms`)
==============================================================================
.. currentmodule:: sknano.core.atoms.atoms
"""
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__docformat__ = 'restructuredtext en'
from collections.abc import Iterable
from operator import attrgetter
import numbers
import re
# import warnings
import numpy as np
from sknano.core import BaseClass, UserList, TabulateMixin, dedupe
from sknano.core.math import convert_condition_str
from sknano.core.refdata import atomic_masses, atomic_mass_symbol_map, \
atomic_numbers, atomic_number_symbol_map, element_symbols, element_names
__all__ = ['Atom', 'Atoms']
[docs]class Atom(BaseClass):
"""Base class for abstract representation of structure atom.
Parameters
----------
element : {str, int}, optional
A string representation of the element symbol or an integer specifying
an element atomic number :math:`\\boldsymbol{Z}`.
"""
_fields = ['element', 'Z', 'mass']
def __init__(self, *args, element=None, mass=None, Z=None, parent=None,
**kwargs):
args = list(args)
if 'm' in kwargs and mass is None:
mass = kwargs['m']
del kwargs['m']
if element is None:
if mass is not None or Z is not None:
if Z is not None:
args.append(Z)
else:
args.append(mass)
if len(args) > 0:
element = args.pop()
super().__init__(*args, **kwargs)
self.mass = mass
self.element = element
self.parent = parent
self.fmtstr = "{element!r}, Z={Z!r}, mass={mass!r}"
def _is_valid_operand(self, other):
return isinstance(other, self.__class__)
def __eq__(self, other):
"""Test equality of two `Atom` object instances."""
if not self._is_valid_operand(other):
return NotImplemented
return (self is other or
(np.allclose([self.Z, self.mass], [other.Z, other.mass]) and
self.element == other.element))
def __le__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
for attr in Atom._fields:
if getattr(self, attr) > getattr(other, attr):
return False
return True
def __lt__(self, other):
"""Test if `self` is *less than* `other`."""
if not self._is_valid_operand(other):
return NotImplemented
# if self.element == other.element == 'X' and \
# self.Z == other.Z == 0:
# test = self.mass < other.mass
# else:
# test = self.Z < other.Z
# return test and self.__le__(other)
for attr in Atom._fields:
if getattr(self, attr) >= getattr(other, attr):
return False
return True
def __ge__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
for attr in Atom._fields:
if getattr(self, attr) < getattr(other, attr):
return False
return True
def __gt__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
# if self.element == other.element == 'X' and \
# self.Z == other.Z == 0:
# test = self.mass > other.mass
# else:
# test = self.Z > other.Z
# return test and self.__ge__(other)
for attr in Atom._fields:
if getattr(self, attr) <= getattr(other, attr):
return False
return True
def __dir__(self):
return ['element', 'Z', 'mass', 'parent']
@property
def Z(self):
"""Atomic number :math:`Z`.
Returns
-------
int
Atomic number :math:`Z`.
"""
return self._Z
@Z.setter
def Z(self, value):
"""Set atomic number :math:`Z`.
Parameters
----------
value : int
Atomic number :math:`Z`.
"""
if not (isinstance(value, numbers.Real) and int(value) > 0):
raise ValueError('Expected a real, positive integer.')
try:
Z = int(value)
idx = Z - 1
symbol = element_symbols[idx]
mass = atomic_masses[symbol]
except KeyError:
print('unrecognized element number: {}'.format(value))
else:
self._Z = atomic_numbers[symbol]
self._mass = mass
self._symbol = symbol
@property
def element(self):
"""Element symbol.
Returns
-------
str
Symbol of chemical element.
"""
return self._symbol
@element.setter
def element(self, value):
"""Set element symbol."""
element_error_msg = 'unrecognized element value: {}'.format(value)
symbol = None
if isinstance(value, str) and re.match(r"[\W]+", value):
raise ValueError(element_error_msg)
if isinstance(value, numbers.Integral):
try:
Z = int(value)
idx = Z - 1
symbol = element_symbols[idx]
except IndexError:
print(element_error_msg)
if symbol is None:
if isinstance(value, str):
if value in element_symbols:
symbol = value
elif value.capitalize() in element_names:
symbol = element_symbols[element_names.index(value)]
elif isinstance(value, numbers.Number):
if value in atomic_mass_symbol_map:
symbol = atomic_mass_symbol_map[value]
elif int(value / 2) in atomic_number_symbol_map:
symbol = atomic_number_symbol_map[int(value / 2)]
if symbol is None:
symbol = 'X'
try:
self._Z = atomic_numbers[symbol]
self._mass = atomic_masses[symbol]
except KeyError:
self._Z = 0
if self.mass is None:
self._mass = 0
self._symbol = symbol
@property
def symbol(self):
"""Element symbol.
Returns
-------
str
Element symbol.
"""
return self._symbol
@property
def mass(self):
"""Atomic mass :math:`m_a` in atomic mass units.
Returns
-------
float
Atomic mass :math:`m_a` in atomic mass units.
"""
return self._mass
@mass.setter
def mass(self, value):
self._mass = value
@property
def m(self):
"""An alias for :attr:`~Atom.mass`."""
return self.mass
@m.setter
def m(self, value):
self.mass = value
[docs] def getattr(self, attr, default=None, recursive=False):
"""Get atom attribute named `attr`.
Parameters
----------
attr : str
Name of attribute
default : :class:`~python:object`, optional
recursive : :class:`~python:bool`, optional
Returns
-------
val : :class:`~python:object`
"""
if recursive:
attr_list = attr.split('.')
obj = self
for attr in attr_list:
obj = getattr(obj, attr, default)
return obj
else:
return getattr(self, attr, default)
[docs] def rezero(self, *args, **kwargs):
assert not hasattr(super(), 'rezero')
[docs] def reset_attrs(self, **kwargs):
"""Reset atom attributes."""
assert not hasattr(super(), 'reset_attrs')
[docs] def update_attrs(self, **kwargs):
"""Update atom attributes."""
assert not hasattr(super(), 'update_attrs')
[docs] def todict(self):
"""Return :class:`~python:dict` of `Atom` constructor parameters."""
return dict(element=self.element, mass=self.mass, Z=self.Z,
parent=self.parent)
[docs]class Atoms(TabulateMixin, UserList):
"""Base class for collection of `Atom` objects.
Parameters
----------
atoms : {None, sequence, `Atoms`}, optional
if not `None`, then a list of `Atom` instance objects or an
existing `Atoms` instance object.
"""
def __init__(self, atoms=None, update_item_class=True, **kwargs):
verbose = kwargs.get('verbose', False)
if atoms is not None and \
(isinstance(atoms, str) or isinstance(atoms, Atom)):
atoms = [atoms]
# if update_item_class and not isinstance(atoms, type(self)) and \
# isinstance(atoms, list):
if update_item_class and isinstance(atoms, Iterable) and \
len(atoms) > 0 and not \
isinstance(atoms[0], self.__atom_class__):
atoms = atoms[:]
for i, atom in enumerate(atoms):
try:
# atoms[i] = self.__atom_class__(**atom.todict())
atomdict = atom.todict()
if verbose and i in list(range(len(atoms), 100)):
print(type(atom))
print(atomdict)
filtered_atomdict = \
{k: atomdict[k] for k in set(dir(atom)) &
set(dir(self.__atom_class__()))}
print('filtered_atomdict: {}'.format(
filtered_atomdict))
atoms[i] = self.__atom_class__(
**{k: atomdict[k] for k in set(dir(atom)) &
set(dir(self.__atom_class__()))})
except AttributeError:
atoms[i] = self.__atom_class__(atom)
super().__init__(initlist=atoms, **kwargs)
@property
def __atom_class__(self):
return Atom
@property
def __item_class__(self):
return self.__atom_class__
def __str__(self):
strrep = self._table_title_str()
objstr = self._obj_mro_str()
if self.data:
items = ('Natoms', 'centroid', 'center_of_mass')
values = [self.Natoms, self.centroid, self.center_of_mass]
table = self._tabulate(list(zip(items, values)))
strrep = '\n'.join((strrep, objstr, table))
return strrep
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return (self is other or self.data == other.data)
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.data < other.data
def __le__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.data <= other.data
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.data != other.data
def __gt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.data > other.data
def __ge__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.data >= other.data
def sort(self, key=attrgetter('element', 'Z', 'mass'), reverse=False):
super().sort(key=key, reverse=reverse)
@classmethod
def _from_iterable(cls, it, **kwargs):
return cls(atoms=it, update_item_class=False, **kwargs)
def _is_valid_operand(self, other):
return isinstance(other, (self.__class__, self.__atom_class__))
def __cast(self, other):
if not isinstance(other, self.__class__):
if not isinstance(other, Iterable):
other = [self.__cast_item(other)]
other = self._from_iterable(other, **self.kwargs)
return other
def __cast_item(self, item):
if not isinstance(item, self.__item_class__):
try:
itemdict = item.todict()
item = self.__item_class__(
**{k: itemdict[k] for k in set(dir(item)) &
set(dir(self.__item_class__()))})
except AttributeError:
item = self.__item_class__(item)
return item
def __setitem__(self, index, item):
if not self._is_valid_operand(item):
if isinstance(index, slice):
item = self.__cast(item)
else:
# try:
# # atomdict = item.todict()
# # atomdict = super().__getitem__(index).todict()
# if isinstance(item, str):
# atomdict['element'] = item
# elif isinstance(item, int):
# atomdict['Z'] = item
# elif isinstance(item, float):
# atomdict['mass'] = item
# else:
# raise ValueError
# item = self.__atom_class__(**atomdict)
# except (IndexError, AttributeError, ValueError):
# item = self.__atom_class__(item)
item = self.__cast_item(item)
super().__setitem__(index, item)
def append(self, atom):
if not self._is_valid_operand(atom):
atom = self.__cast_item(atom)
super().append(atom)
def insert(self, i, atom):
if not self._is_valid_operand(atom):
atom = self.__cast_item(atom)
super().insert(i, atom)
def __add__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
atoms = self.data + \
list(atom for atom in self.__cast(other) if atom not in self)
return self._from_iterable(atoms, **self.kwargs)
def __radd__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
other = self.__cast(other)
atoms = other.data + list(atom for atom in self if atom not in other)
return self._from_iterable(atoms, **self.kwargs)
def __iadd__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
self.data += list(atom for atom in self.__cast(other)
if atom not in self)
return self
def __sub__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
return self._from_iterable((atom for atom in self
if atom not in self.__cast(other)),
**self.kwargs)
def __rsub__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
return self._from_iterable((atom for atom in self.__cast(other)
if atom not in self),
**self.kwargs)
def __isub__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
self.data -= list(atom for atom in self.__cast(other) if atom in self)
return self
def __and__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
return self._from_iterable((atom for atom in self.__cast(other)
if atom in self), **self.kwargs)
__rand__ = __and__
def __iand__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
self.data -= list(atom for atom in self.__cast(other)
if atom not in self)
return self
def __or__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
# atoms = self.data + list(atom for atom in other if atom not in self)
atoms = dedupe((atom for atoms in (self, self.__cast(other))
for atom in atoms), key=attrgetter('id'))
return self._from_iterable(atoms, **self.kwargs)
__ror__ = __or__
def __ior__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
self.data += \
list(atom for atom in self.__cast(other) if atom not in self)
return self
def __xor__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
other = self.__cast(other)
return (self - other) | (other - self)
__rxor__ = __xor__
def __ixor__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
if other is self:
self.clear()
else:
other = self.__cast(other)
for atom in other:
if atom in self:
self.remove(atom)
else:
self.append(atom)
return self
@property
def Natoms(self):
"""Number of atoms in `Atoms`."""
return len(self)
@property
def M(self):
"""Total mass of `Atoms`."""
# return math.fsum(self.masses)
return self.masses.sum()
@property
def elements(self):
""":class:`~numpy:numpy.ndarray` of :attr:`Atom.element`\ s."""
return np.asarray([atom.element for atom in self])
@property
def masses(self):
""":class:`~numpy:numpy.ndarray` of :attr:`Atom.mass`\ s."""
return np.asarray([atom.mass for atom in self])
@property
def symbols(self):
""":class:`~numpy:numpy.ndarray` of :attr:`Atom.symbol`\ s."""
return np.asarray([atom.symbol for atom in self])
def filter(self, condition, invert=False):
"""Filter `Atoms` by `condition`.
.. versionchanged:: 0.3.11
Filters the list of `Atoms` **in-place**. Use
:meth:`~Atoms.filtered` to generate a new filtered list
of `Atoms`.
Parameters
----------
condition : :class:`~python:str` or boolean array
Boolean index array having same shape as the initial dimensions
of the list of `Atoms` being indexed.
invert : bool, optional
If `True`, the boolean array `condition` is inverted element-wise.
"""
if isinstance(condition, str):
condition = convert_condition_str(self, condition)
if invert:
condition = ~condition
try:
self.data = np.asarray(self)[condition].tolist()
except AttributeError:
self.data = np.asarray(self)[condition]
def filtered(self, condition, invert=False):
"""Return new list of `Atoms` filtered by `condition`.
.. versionadded:: 0.3.11
Parameters
----------
condition : array_like, bool
Boolean index array having same shape as the initial dimensions
of the list of `Atoms` being indexed.
invert : bool, optional
If `True`, the boolean array `condition` is inverted element-wise.
Returns
-------
filtered_atoms : `Atoms`
If `invert` is `False`, return the elements where `condition`
is `True`.
If `invert` is `True`, return the elements where `~condition`
(i.e., numpy.invert(condition)) is `True`.
Examples
--------
An example using the structure data of a 10 nm `(10, 0)`
`SWCNT`:
>>> from sknano.generators import SWNTGenerator
>>> swnt = SWNTGenerator(10, 0, Lz=10, fix_Lz=True).atoms
>>> # select 'left', 'middle', 'right' atoms
>>> latoms = swnt.filtered(swnt.z <= 25)
>>> matoms = swnt.filtered((swnt.z < 75) & (swnt.z > 25))
>>> ratoms = swnt.filtered(swnt.z >= 75)
>>> from pprint import pprint
>>> pprint([getattr(atoms, 'bounds') for atoms in
... (latoms, matoms, ratoms)])
[Cuboid(pmin=Point([-3.914435, -3.914435, 0.0]), \
pmax=Point([3.914435, 3.914435, 24.85])),
Cuboid(pmin=Point([-3.914435, -3.914435, 25.56]), \
pmax=Point([3.914435, 3.914435, 74.55])),
Cuboid(pmin=Point([-3.914435, -3.914435, 75.97]), \
pmax=Point([3.914435, 3.914435, 100.11]))]
>>> latoms.Natoms + matoms.Natoms + ratoms.Natoms == swnt.Natoms
True
"""
if isinstance(condition, str):
condition = convert_condition_str(self, condition)
if invert:
condition = ~condition
try:
return self.__class__(atoms=np.asarray(self)[condition].tolist(),
**self.kwargs)
except AttributeError:
return self.__class__(atoms=np.asarray(self)[condition],
**self.kwargs)
def select(self, selstr=None, selstrlist=None, verbose=False):
"""Return `Atom` or `Atoms` from selection command.
Parameters
----------
selstr : :class:`~python:str`, optional
optional if `selstrlist` is not `None`
selstrlist : {`None`, :class:`~python:list`}, optional
:class:`~python:list` of selection strings.
Returns
-------
:class:`~python:list` of `Atom` or `Atoms` objects
if `selstrlist` is not `None`
:class:`Atom` or :class:`Atoms` if `selstr` is not `None`
"""
from .selections import SelectionParser, SelectionException
if selstrlist is not None:
selections = []
for selstr in selstrlist:
try:
selections.append(self.select(selstr, verbose=verbose))
except SelectionException as e:
print(e)
return selections
elif selstr is not None:
try:
return SelectionParser(self, verbose=verbose).parse(selstr)
except SelectionException as e:
print(e)
return None
else:
return self.__class__()
def get_atoms(self, asarray=False, aslist=True):
"""Return `Atoms` either as list (default) or numpy array or self.
Parameters
----------
asarray, aslist : :class:`~python:bool`, optional
Default values: `asarray=False`, `aslist=True`
Returns
-------
:class:`~numpy:numpy.ndarray` if `asarray` is `True`
:class:`~python:list` if `asarray` is `False` and `aslist` is `True`
:class:`Atoms` object if `asarray` and `aslist` are `False`
"""
if asarray:
return np.asarray(self.data)
elif aslist:
return self.data
else:
return self
def getattr(self, attr, default=None, recursive=False):
"""Get :class:`~numpy:numpy.ndarray` of atom attributes `attr`.
Parameters
----------
attr : str
Name of attribute to pass to `getattr` from each `Atom` in
`Atoms`.
default : :class:`~python:object`, optional
recursive : :class:`~python:bool`, optional
Returns
-------
:class:`~numpy:numpy.ndarray`
"""
if recursive:
attr_values = []
attr_list = attr.split('.')
for atom in self:
obj = atom
for attr in attr_list:
obj = getattr(obj, attr, default)
attr_values.append(obj)
return np.asarray(attr_values)
else:
return np.asarray([getattr(atom, attr, default) for atom in self])
def mapatomattr(self, from_attr=None, to_attr=None, attrmap=None):
"""Set/update atom attribute from another atom attribute with dict.
.. versionchanged:: 0.3.11
Made all arguments required keyword arguments and reversed the
order of the former positional arguments `from_attr` and
`to_attr` to be more natural and consistent with the key, value
pairs in the `attrmap` dictionary.
Parameters
----------
from_attr, to_attr : :class:`python:str`
attrmap : :class:`python:dict`
Examples
--------
Suppose you have an `Atoms` instance named ``atoms`` that contains
`Atom` instances of two atom types `1` and `2` and we want to set
all `Atom`\ s with `type=1` to Nitrogen and all `Atom`\ s with
`type=2` to Argon. In other words, we want to use the
:attr:`~sknano.core.atoms.TypeAtom.type` attribute to set the
:attr:`~Atom.element` attribute, which we do by passing a
:class:`~python:dict` mapping each `type` to the respective element
symbol. For example::
>>> atoms.mapatomattr('type', 'element', {1: 'N', 2: 'Ar'})
"""
try:
[setattr(atom, to_attr, attrmap[getattr(atom, from_attr)])
for atom in self if getattr(atom, from_attr) is not None]
except (KeyError, TypeError) as e:
print(e)
def rezero(self, epsilon=1.0e-10):
"""Set values with absolute value less than `epsilon` to zero.
Calls the `rezero` method on each `atom` in `self`.
Parameters
----------
epsilon : :class:`~python:float`
values with absolute value less than `epsilon` are set to zero.
"""
[atom.rezero(epsilon=epsilon) for atom in self]
def reset_attrs(self, **kwargs):
"""Call corresponding `reset_attrs` method on each atom"""
# [atom.reset_attrs(**kwargs) for atom in self]
assert not hasattr(super(), 'reset_attrs')
def update_attrs(self, **kwargs):
"""Call `update_attrs` method on each atom."""
# [atom.update_attrs() for atom in self]
# if len(kwargs) != 0:
# warnings.warn('`Atoms.update_attrs` received unused kwargs: \n'
# '{}'.format(kwargs))
assert not hasattr(super(), 'update_attrs')