# -*- coding: utf-8 -*-
"""
===============================================================================
Atom classes for NN analysis (:mod:`sknano.core.atoms.neighbor_atoms`)
===============================================================================
.. currentmodule:: sknano.core.atoms.neighbor_atoms
"""
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__docformat__ = 'restructuredtext en'
from collections import Counter, OrderedDict
import numbers
import numpy as np
try:
from scipy.spatial import KDTree
# from scipy.spatial import cKDTree as KDTree
except ImportError:
raise ImportError('Install scipy version >= 0.13.0 to allow '
'nearest-neighbor queries between atoms.')
from sknano.core import ordinal_form
from sknano.core.analysis import PeriodicKDTree
from sknano.core.math import Vector, Vectors
from .atoms import Atom, Atoms
# from .cn_atoms import CNAtom
from .mixins import KDTreeAtomsMixin
__all__ = ['NeighborAtom', 'NeighborAtoms']
[docs]class NeighborAtom(Atom):
"""An `Atom` sub-class for neighbor analysis.
Parameters
----------
neighbors : :class:`Atoms` sub-class
neighbor_map : :class:`~python:dict`
.. todo::
Deprecate/replace `neighbors` parameter with `neighbor_map`
"""
def __init__(self, *args, neighbors=None, neighbor_map=None, **kwargs):
super().__init__(*args, **kwargs)
self.neighbors = neighbors
self.neighbor_map = neighbor_map
self.nn_adjacency_map = {}
# self.nn_adjacency_list = []
# self.fmtstr = super().fmtstr + ", neighbors={neighbors!r}"
def __dir__(self):
attrs = super().__dir__()
attrs.extend(['neighbors', 'neighbor_map'])
return attrs
@property
def neighbors(self):
"""Neighbor atoms."""
return self._neighbors
@neighbors.setter
def neighbors(self, value):
if value is not None and not isinstance(value, Atoms):
raise TypeError('Expected an `Atoms` object.')
self._neighbors = value
@neighbors.deleter
def neighbors(self):
del self._neighbors
@property
def neighbor_distances(self):
"""Neighbor atom distances."""
return self.neighbors.distances
# @neighbor_distances.setter
# def neighbor_distances(self, value):
# self._neighbor_distances = np.asarray(value)
@property
def CN(self):
"""`NeighborAtom` coordination number."""
try:
return self.neighbors.Natoms
except AttributeError:
return 0
@property
def Nneighbors(self):
"""Number of neighbors."""
return self.neighbors.Natoms
@property
def NN(self):
"""Nearest-neighbor `Atoms`."""
try:
return self.neighbors
except AttributeError:
return None
@NN.setter
def NN(self, value):
"""Set nearest-neighbor `Atoms`."""
self.neighbors = value
@NN.deleter
def NN(self):
del self.neighbors
@property
def neighbor_map(self):
"""Neighbor atoms."""
return self._neighbor_map
@neighbor_map.setter
def neighbor_map(self, value):
if value is None:
value = OrderedDict()
elif not isinstance(value, OrderedDict):
raise TypeError('Expected an `OrderedDict` object.')
for n, neighbors in value.items():
setattr(self, '_{}_neighbors'.format(n), neighbors)
self._neighbor_map = value
@neighbor_map.deleter
def neighbor_map(self):
del self._neighbor_map
@property
def first_neighbors(self):
"""First nearest neighbors."""
return self.get_nth_nearest_neighbors(1)
@first_neighbors.setter
def first_neighbors(self, value):
self.set_nth_nearest_neighbors(1, value)
@property
def second_neighbors(self):
"""Second nearest neighbors"""
return self.get_nth_nearest_neighbors(2)
@second_neighbors.setter
def second_neighbors(self, value):
self.set_nth_nearest_neighbors(2, value)
@property
def third_neighbors(self):
"""Third nearest neighbors"""
return self.get_nth_nearest_neighbors(3)
@third_neighbors.setter
def third_neighbors(self, value):
self.set_nth_nearest_neighbors(3, value)
[docs] def get_nth_nearest_neighbors(self, n, exclusive=True):
"""Get the `n`th set of neighbors."""
if exclusive:
return getattr(self, '_{}_neighbors'.format(ordinal_form(n)))
else:
neighbors = self.__class__(**self.kwargs)
for i in range(1, n + 1):
neighbors.extend(
getattr(self, '_{}_neighbors'.format(ordinal_form(n))))
return neighbors
[docs] def set_nth_nearest_neighbors(self, n, neighbors):
"""Set nth nearest neighbors"""
# Since we're setting the nth set of neighbors, we will remove
# all 1..n-1 neighbors from the nth set.
# distances = distances
# neighbors = neighbors
neighbor_map = self.neighbor_map
for i in range(1, n):
for neighbor in self.get_nth_nearest_neighbors(i):
if neighbor in neighbors:
# print(neighbors.index(neighbor))
# distances.remove(neighbors.index(neighbor))
neighbors.remove(neighbor)
# neighbors.distances = distances
# print(type(neighbors))
setattr(self, '_{}_neighbors'.format(ordinal_form(n)), neighbors)
neighbor_map[ordinal_form(n)] = neighbors
[docs] def todict(self):
"""Return :class:`~python:dict` of constructor parameters."""
super_dict = super().todict()
super_dict.update(dict(neighbors=self.neighbors,
neighbor_map=self.neighbor_map))
return super_dict
[docs]class NeighborAtoms(KDTreeAtomsMixin, Atoms):
"""An `Atoms` sub-class for neighbor analysis.
Parameters
----------
atoms : {None, sequence, `NeighborAtoms`}, optional
if not `None`, then a list of `StructureAtom` instance objects or an
existing `NeighborAtoms` instance object.
kNN : :class:`~python:int`
Number of nearest neighbors to return when querying the kd-tree.
NNrc : :class:`~python:float`
Nearest neighbor radius cutoff.
neighbor_cutoffs : :class:`~python:list`, optional
"""
def __init__(self, *args, kNN=16, NNrc=2.0, neighbor_cutoffs=None,
neighbors_analyzed=False, **kwargs):
super().__init__(*args, **kwargs)
self.kNN = kNN
self.NNrc = NNrc
self.neighbor_cutoffs = neighbor_cutoffs
self.neighbors_analyzed = neighbors_analyzed
self.idx = []
self.nn_idx = []
self._nn_adjacency_matrix = None
self._nn_adjacency_map = None
self._nn_adjacency_list = None
self._nn_vectors = None
self._nn_seed = None
@property
def __atom_class__(self):
return NeighborAtom
@property
def atom_tree(self):
"""Concrete implementation of :attr:`~KDTreeAtomsMixin.atom_tree`."""
try:
try:
if np.any(self.pbc):
boxsize = np.asarray(self.lattice.lengths)
for i, pbc in enumerate(self.pbc):
if not pbc:
boxsize[i] = -1
return PeriodicKDTree(np.asarray(self.coords),
boxsize=boxsize,
lattice=self.lattice)
except AttributeError:
pass
else:
return KDTree(np.asarray(self.coords))
except ValueError as e:
print(e)
return None
@property
def kNN(self):
"""Max number of nearest-neighbors to return from kd-tree search."""
return self._kNN
@kNN.setter
def kNN(self, value):
if not isinstance(value, numbers.Number):
raise TypeError('Expected an integer >= 0')
self._kNN = self.kwargs['kNN'] = int(value)
@property
def NNrc(self):
"""Nearest neighbor radius cutoff."""
return self._NNrc
@NNrc.setter
def NNrc(self, value):
if not (isinstance(value, numbers.Number) and value >= 0):
raise TypeError('Expected a real number greater >= 0')
self._NNrc = self.kwargs['NNrc'] = value
@property
def neighbor_cutoffs(self):
"""Nearest neighbor radius cutoff."""
return self._neighbor_cutoffs
@neighbor_cutoffs.setter
def neighbor_cutoffs(self, cutoffs):
if cutoffs is None:
cutoffs = []
if not any([np.allclose(self.NNrc, cutoff) for cutoff in cutoffs]):
cutoffs.append(self.NNrc)
self._neighbor_cutoffs = self.kwargs['neighbor_cutoffs'] = \
sorted(np.asarray(cutoffs[:], dtype=float).tolist())
@property
def neighbors_analyzed(self):
"""Return `True` if neighbors have been analyzed."""
return self._neighbors_analyzed
@neighbors_analyzed.setter
def neighbors_analyzed(self, value):
if not isinstance(value, bool):
raise ValueError('Expected a boolean value.')
self._neighbors_analyzed = self.kwargs['neighbors_analyzed'] = value
@property
def coordination_numbers(self):
""":class:`~numpy:numpy.ndarray` of :attr:`NeighborAtom.CN`\ s."""
return np.asarray([atom.CN for atom in self])
@property
def coordination_number_counts(self):
"""Coordination number counts.
Returns
-------
:class:`~python:collections.Counter`
:class:`~python:collections.Counter` of
:attr:`~NeighborAtoms.coordination_numbers`.
"""
return Counter(self.coordination_numbers)
@property
def coordination_counts(self):
"""Alias for :attr:`~NeighborAtoms.coordination_number_counts`."""
return self.coordination_number_counts
@property
def CN_counts(self):
"""Alias for :attr:`~NeighborAtoms.coordination_number_counts`."""
return self.coordination_number_counts
@property
def neighbors(self):
"""Return array of neighbor atoms."""
return np.asarray([atom.neighbors for atom in self])
@property
def distances(self):
"""Neighbor atoms distances."""
return self._distances
@distances.setter
def distances(self, value):
self._distances = np.asarray(value)
@property
def neighbor_distances(self):
"""Neighbor distances"""
distances = []
# [distances.extend(atom.neighbor_distances.tolist()) for atom in self]
[distances.extend(atom.neighbors.distances.tolist()) for atom in self]
return np.asarray(distances)
@property
def NNN(self):
"""Number of first nearest-neighbors."""
return len(self.nn_idx)
@property
def nearest_neighbors(self):
"""Return array of nearest-neighbor atoms for each `KDTAtom`."""
# self._update_neighbors()
return np.asarray([atom.NN for atom in self])
@property
def first_neighbors(self):
"""First neighbors."""
return [atom.first_neighbors for atom in self]
@property
def second_neighbors(self):
"""Second neighbors."""
return [atom.second_neighbors for atom in self]
@property
def third_neighbors(self):
"""Third neighbors."""
return [atom.third_neighbors for atom in self]
def get_nth_nearest_neighbors(self, n, exclusive=True):
"""Return `n`th nearest neighbors."""
return [getattr(atom, '_{}_neighbors'.format(ordinal_form(n)))
for atom in self]
# def reset_attrs(self):
# """Reset :class:`NeighborAtoms` attributes."""
# super().reset_attrs()
def update_attrs(self, **kwargs):
"""Update :class:`NeighborAtoms` attributes."""
super().update_attrs(**kwargs)
self.update_neighbors(**kwargs)
def update_neighbors(self, **kwargs):
"""Update :attr:`NeighborAtom.neighbors`."""
cutoffs = kwargs.pop('cutoffs', None)
if 'cutoff' in kwargs and cutoffs is None:
cutoffs = kwargs.pop('cutoff')
if cutoffs is None:
cutoffs = self.neighbor_cutoffs[:]
elif not isinstance(cutoffs, list):
cutoffs = [cutoffs]
self.neighbor_cutoffs = cutoffs
if not self._neighbors_analyzed:
self.neighbors_analyzed = True
for n, cutoff in enumerate(self.neighbor_cutoffs, start=1):
try:
NNd, NNi = self.query_atom_tree(k=self.kNN, rc=cutoff)
for i, atom in enumerate(self):
neighbors = \
self.__class__([self[NNi[i][j]] for j, d in
enumerate(NNd[i]) if d <= cutoff],
update_item_class=False, **self.kwargs)
distances = \
[NNd[i][j] for j, d in enumerate(NNd[i])
if d <= cutoff]
neighbors.distances = distances
if np.allclose(cutoff, self.NNrc):
atom.neighbors = neighbors
atom.set_nth_nearest_neighbors(n, neighbors[:])
except ValueError:
pass
def update_neighbor_lists(self):
"""Update neighbor lists"""
self._update_nn_lists()
self._update_nn_seed()
self._update_nn_vectors()
self._update_nn_adjacency_matrix()
@property
def nn_adjacency_matrix(self):
"""Return nearest-neighbor adjacency matrix."""
if self._nn_adjacency_matrix is None:
self._update_nn_adjacency_matrix()
return self._nn_adjacency_matrix
@property
def nn_adjacency_map(self):
"""Return nearest-neighbor adjacency map"""
if self._nn_adjacency_map is None:
self._update_nn_adjacency_map()
return self._nn_adjacency_map
@property
def nn_adjacency_list(self):
"""Return nearest-neighbor adjacency list"""
if self._nn_adjacency_list is None:
self._update_nn_adjacency_list()
return self._nn_adjacency_list
@property
def nn_seed(self):
"""Return nearest-neighbor seed list"""
if self._nn_seed is None:
self._update_nn_seed()
return self._nn_seed
@property
def nn_vectors(self):
"""Return nearest-neighbor vectors."""
if self._nn_vectors is None:
self._update_nn_vectors()
return self._nn_vectors
def _update_nn_lists(self, NNi=None, cutoff=None):
if NNi is None:
if cutoff is None:
cutoff = self.NNrc
_, NNi = self.query_atom_tree(k=self.kNN, rc=cutoff)
Natoms = self.Natoms
idx = []
nn_idx = []
for i, nn_indices in enumerate(NNi):
for ni in nn_indices[:]:
if ni < Natoms:
idx.append(i)
nn_idx.append(ni)
self.idx = np.asarray(idx, dtype=int)
self.nn_idx = np.asarray(nn_idx, dtype=int)
def _update_nn_seed(self):
n = self.Natoms
nnn = self.NNN
idx = self.idx
nn_seed = (n + 1) * [0]
for k in range(n):
nn_seed[k] = -1
nn_seed[n] = nnn
nn_seed[idx[0]] = 0
for k in range(1, nnn):
if idx[k] != idx[k - 1]:
nn_seed[idx[k]] = k
self._nn_seed = np.asarray(nn_seed, dtype=int)
def _update_nn_adjacency_matrix(self):
Natoms = self.Natoms
nn_seed = self.nn_seed
nn_idx = self.nn_idx
nn_adjacency_matrix = np.zeros((Natoms, Natoms), dtype=int)
# self._update_nn_lists()
def _update_nn_adjacency_map(nn_map, indices, nindices):
for i in indices:
for ni in nindices:
if ni not in nn_map:
nn_map[ni] = nn_map[i] + 1
for i, atom in enumerate(self):
nn_map = atom.nn_adjacency_map = {}
nn_map[i] = 0
indices = [i]
nindices = [nn_idx[si] for si in range(nn_seed[i], nn_seed[i+1])]
_update_nn_adjacency_map(nn_map, indices, nindices)
while len(nindices) > 0:
indices = nindices[:]
nindices = []
for ni in indices[:]:
nindices.extend(
[nn_idx[nsi] for nsi in
range(nn_seed[ni], nn_seed[ni+1])
if nn_idx[nsi] not in nn_map])
_update_nn_adjacency_map(nn_map, indices, nindices)
[nn_adjacency_matrix[i].__setitem__(j, nn_map[j])
for j in range(Natoms) if j in nn_map]
self._nn_adjacency_matrix = nn_adjacency_matrix
def _update_nn_adjacency_map(self):
Natoms = self.Natoms
nn_idx = self.nn_idx
nn_seed = self.nn_seed
nn_adjacency_map = OrderedDict()
for i in range(Natoms):
nn_adjacency_map[i] = \
[nn_idx[si] for si in range(nn_seed[i], nn_seed[i+1])]
self._nn_adjacency_map = nn_adjacency_map
def _update_nn_adjacency_list(self):
Natoms = self.Natoms
nn_idx = self.nn_idx
nn_seed = self.nn_seed
nn_adjacency_list = []
for i in range(Natoms):
nn_adjacency_list.append(
[nn_idx[si] for si in range(nn_seed[i], nn_seed[i+1])])
self._nn_adjacency_list = nn_adjacency_list
def _update_nn_vectors(self):
# from sknano.core.crystallography import pbc_diff
idx = self.idx
nn_idx = self.nn_idx
nn_vectors = Vectors()
try:
lattice = self.lattice
pbc = np.any(self.pbc)
if not pbc or lattice is None:
raise AttributeError
except AttributeError:
for i, ni in zip(idx, nn_idx):
r = self[ni].r - self[i].r
nn_vectors.append(r)
else:
for i, ni in zip(idx, nn_idx):
fdiff = lattice.fractional_diff(self[ni].rs, self[i].rs)
dr = Vector(lattice.fractional_to_cartesian(fdiff))
nn_vectors.append(dr)
self._nn_vectors = nn_vectors
def count_neighbors_in_self(self, r, p=2.0):
"""Count number of neighbor pairs for each atom in self.
Parameters
----------
r : float or one-dimensional array of floats
p : float, 1<=p<=infinity, optional
Returns
-------
result : 1-D array of ints
"""
r = np.asarray(r)
ids = self.ids
# if np.any(self.pbc):
# boxsize = np.asarray(self.lattice.lengths)
# return PeriodicKDTree(np.asarray(self.coords), boxsize=boxsize,
# lattice=self.lattice)
return np.asarray([KDTree([atom.r]).count_neighbors(
self.filtered(ids != atom.id).atom_tree,
r, p=p) for atom in self])