Source code for sisl.geom._neighbors._neighborlists

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from __future__ import annotations

from functools import cached_property
from numbers import Integral
from typing import Optional

import numpy as np

from sisl import Geometry
from sisl._internal import set_module

__all__ = [
    "UniqueNeighborList",
    "FullNeighborList",
    "PartialNeighborList",
    "AtomNeighborList",
    "CoordsNeighborList",
    "CoordNeighborList",
]


@set_module("sisl.geom")
class Neighbors:

    def __init__(
        self,
        geometry: Geometry,
        finder_results: np.ndarray,
        split_indices: Optional[np.ndarray] = None,
    ):
        self.geometry = geometry
        self._finder_results = finder_results
        self._split_indices = split_indices

    @property
    def I(self) -> np.ndarray:
        """For each neighbor pair (I, J), the first index."""
        return self._finder_results[:, 0]

    @property
    def i(self) -> np.ndarray:
        """Same as `I`, provided for backwards compatibility, may be deprecated later"""
        return self.I

    @property
    def J(self) -> np.ndarray:
        """For each neighbor pair (I, J), the second index."""
        return self._finder_results[:, 1]

    @property
    def j(self) -> np.ndarray:
        """Same as `J`, provided for backwards compatibility, may be deprecated later"""
        return self.J

    @property
    def isc(self) -> np.ndarray:
        r"""For each neighbor pair (I, J), the supercell indices of :math:`J`."""
        return self._finder_results[:, 2:]

    @cached_property
    def min_nsc(self) -> np.ndarray:
        """The minimum nsc for an auxiliary supercell to contain all neighbors in this object."""
        if len(self.isc) == 0:
            return np.ones(3, dtype=int)
        else:
            return np.max(np.abs(self.isc), axis=0) * 2 + 1

    @property
    def n_neighbors(self):
        raise NotImplementedError

    @cached_property
    def split_indices(self) -> np.ndarray:
        """Indices to split the interactions of each atom."""
        if self._split_indices is None:
            return np.cumsum(self.n_neighbors)
        else:
            return self._split_indices

    def __len__(self):
        return len(self.split_indices)


@set_module("sisl.geom")
class AtomsNeighborList(Neighbors):
    """Base class for interactions between atoms."""

    @cached_property
    def n_neighbors(self) -> np.ndarray:
        """Number of neighbors that each atom has."""

        if self._split_indices is None:
            n_neighbors = np.zeros(self.geometry.na, dtype=int)
            index, counts = np.unique(self.I, return_counts=True)

            n_neighbors[index] = counts
            return n_neighbors
        else:
            return np.diff(self._split_indices, prepend=0)


@set_module("sisl.geom")
class UniqueNeighborList(AtomsNeighborList):
    """Full neighbors list of a system, but **containing only the upper triangle of the adjacency matrix**.

    What this means, is that the object only contains one direction of each interaction.

    This is only possible if the interaction is symmetric (there is no directionality and
    thresholds for interaction do not depend on direction).

    Examples
    --------

    You can get a unique neighbors list from the `find_unique_pairs` method of a `NeighborFinder` object.
    Then, you can retrieve the neighbors from it:

    .. code-block:: python

        import sisl

        # Build a graphene supercell with a vacancy
        graphene = sisl.geom.graphene().tile(2, 0).tile(2, 1)
        graphene = graphene.remove(2).translate2uc()

        # Initialize a finder for neighbors that are within 1.5 Angstrom
        finder = sisl.geom.NeighborFinder(graphene, R=1.5)

        # Get the list of unique neighbor pairs
        neighbors = finder.find_unique_pairs()

        # You can get the neighbor pairs (I,J) from the I and J attributes
        # The supercell index of atom J is in the isc attribute.
        print("ATOM I", neighbors.I)
        print("ATOM J (NEIGHBOR)", neighbors.J)
        print("NEIGHBORS ISC:", neighbors.isc)

        # Notice that I is always smaller than J. Each connection is only
        # stored once.
        # You can convert to a full neighbors list, which will contain both
        # directions.
        full_neighbors = neighbors.to_full()

    """

[docs] @cached_property def n_neighbors(self) -> np.ndarray: """Number of neighbors that each atom has.""" if self._split_indices is None: n_neighbors = np.zeros(self.geometry.na, dtype=int) index, counts = np.unique([self.I, self.J], return_counts=True) n_neighbors[index] = counts return n_neighbors else: return np.diff(self._split_indices, prepend=0)
[docs] def to_full(self) -> FullNeighborList: """Converts the unique neighbors list to a full neighbors list.""" upper_tri = self._finder_results lower_tri = np.column_stack( [self.J, self.I, -self.isc[:, 0], -self.isc[:, 1], -self.isc[:, 2]] ) self_interactions = (self.I == self.J) & np.all(self.isc == 0, axis=1) lower_tri = lower_tri[~self_interactions] # Concatenate the lower triangular with the upper triangular part all_finder_results = np.concatenate([upper_tri, lower_tri], axis=0) # Sort by I and then by J sorted_indices = np.lexsort(all_finder_results[:, [1, 0]].T) all_finder_results = all_finder_results[sorted_indices] return FullNeighborList( self.geometry, all_finder_results, )
@set_module("sisl.geom") class FullNeighborList(AtomsNeighborList): r"""Full neighbors list of a system. This class, contrary to `UniqueNeighborList`, (possibly) contains the two directions of an interaction between two given atoms. Notice that it is possible that there is a connection from atom :math:`I` to atom :math:`J` but not the other way around. Examples -------- You can get a full neighbors list from the `find_neighbors` method of a `NeighborFinder` object. Then, you can retreive the neighbors from it: .. code-block:: python import sisl # Build a graphene supercell with a vacancy graphene = sisl.geom.graphene().tile(2, 0).tile(2, 1) graphene = graphene.remove(2).translate2uc() # Initialize a finder for neighbors that are within 1.5 Angstrom finder = sisl.geom.NeighborFinder(graphene, R=1.5) # Get the full neighbors list neighbors = finder.find_neighbors() # You can loop through atoms to get their neighbors for at_neighs in neighbors: print() print(f"NEIGHBORS OF ATOM {at_neighs.atom} ({at_neighs.n_neighbors} neighbors): ") print("J", at_neighs.J) print("ISC", at_neighs.isc) # Or get the neighbors of a particular atom: neighbors[0].J See Also -------- AtomNeighborList The object returned by this list when iterating or indexing. """ def __getitem__(self, item) -> AtomNeighborList: """Returns the interactions of a given atom.""" if isinstance(item, Integral): start = 0 if item == 0 else self.split_indices[item - 1] end = self.split_indices[item] return AtomNeighborList( self.geometry, self._finder_results[start:end], atom=item, ) else: raise ValueError("Only integer indexing is supported.")
[docs] def to_unique(self) -> UniqueNeighborList: """Converts the full neighbors list to a unique neighbors list.""" full_finder_results = self._finder_results unique_finder_results = full_finder_results[self.I <= self.J] # Concatenate the uc connections with the rest of the connections. return UniqueNeighborList( geometry=self.geometry, finder_results=unique_finder_results )
@set_module("sisl.geom") class PartialNeighborList(AtomsNeighborList): """Neighbors list containing only the neighbors of some atoms. Examples -------- You can get a partial neighbors list from the `find_neighbors` method of a `NeighborFinder` object if you pass the `atoms` argument. Then, you can retrieve the neighbors from it: .. code-block:: python import sisl # Build a graphene supercell with a vacancy graphene = sisl.geom.graphene().tile(2, 0).tile(2, 1) graphene = graphene.remove(2).translate2uc() # Initialize a finder for neighbors that are within 1.5 Angstrom finder = sisl.geom.NeighborFinder(graphene, R=1.5) # Get a partial neighbors list neighbors = finder.find_neighbors(atoms=[2, 4]) # You can loop through atoms to get their neighbors # In this case, the loop will go through atoms 2 and 4 for at_neighs in neighbors: print() print(f"NEIGHBORS OF ATOM {at_neighs.atom} ({at_neighs.n_neighbors} neighbors): ") print("J", at_neighs.J) print("ISC", at_neighs.isc) # Or get the neighbors of a particular atom neighbors[0].atom # This will be 2 neighbors[0].J See Also -------- AtomNeighborList The object returned by this list when iterating or indexing. """ atoms: np.ndarray """The atoms for which the neighbors are stored.""" def __init__( self, geometry: Geometry, finder_results, atoms: np.ndarray, split_indices=None ): self.atoms = atoms super().__init__(geometry, finder_results, split_indices)
[docs] @cached_property def n_neighbors(self) -> np.ndarray: """Number of neighbors that each atom has.""" if self._split_indices is None: return np.array([np.sum(self.I == at) for at in self.atoms]) else: return np.diff(self._split_indices, prepend=0)
def __getitem__(self, item) -> AtomNeighborList: """Returns the interactions of a given atom.""" if isinstance(item, Integral): start = 0 if item == 0 else self.split_indices[item - 1] end = self.split_indices[item] return AtomNeighborList( self.geometry, self._finder_results[start:end], atom=self.atoms[item], ) else: raise ValueError("Only integer indexing is supported.") @set_module("sisl.geom") class AtomNeighborList(Neighbors): """List of atoms that are close to a given atom. The usual way to get an `AtomNeighborList` object is by iterating over a `FullNeighborList` or a `PartialNeighborList`. See their documentation for examples. See Also -------- FullNeighborList, PartialNeighborList The lists that, when iterated, return `AtomNeighborList` objects. """ #: The atom for which the neighbors are stored. atom: int """The atom for which the neighbors are stored.""" def __init__(self, geometry: Geometry, finder_results, atom: int): self.atom = atom super().__init__(geometry, finder_results)
[docs] @cached_property def n_neighbors(self) -> int: """Number of neighbors of the atom.""" return len(self._finder_results)
@set_module("sisl.geom") class CoordsNeighborList(Neighbors): """List of atoms that are close to a set of coordinates in space. Examples -------- You can get a coordinates neighbors list from the `find_close` method of a `NeighborFinder` object. Then, you can retrieve the neighbors from it: .. code-block:: python import sisl # Build a graphene supercell with a vacancy graphene = sisl.geom.graphene().tile(2, 0).tile(2, 1) graphene = graphene.remove(2).translate2uc() # Initialize a finder for neighbors that are within 1.5 Angstrom finder = sisl.geom.NeighborFinder(graphene, R=1.5) # Get the full neighbors list coords = [[0, 0, 0], [2, 0, 0]] neighbors = finder.find_close(coords) # You can loop through coordinates to get their neighbors for coord_neighs in neighbors: print() print(f"NEIGHBORS OF COORDINATE {coord_neighs.xyz} ({coord_neighs.n_neighbors} neighbors): ") print("J", coord_neighs.J) print("ISC", coord_neighs.isc) # Or get the neighbors of a particular coordinate: neighbors[0].J See Also -------- CoordNeighborList The object returned by this list when iterating or indexing. """ def __init__(self, geometry, xyzs: np.ndarray, finder_results, split_indices=None): self.xyzs = np.atleast_2d(xyzs) super().__init__(geometry, finder_results, split_indices)
[docs] @cached_property def n_neighbors(self) -> np.ndarray: """Number of atoms that are close to each coordinate""" if self._split_indices is None: n_neighbors = np.zeros(len(self.xyzs), dtype=int) index, counts = np.unique(self.I, return_counts=True) n_neighbors[index] = counts return n_neighbors else: return np.diff(self._split_indices, prepend=0)
def __getitem__(self, item): """Returns the interactions of a given coordinate.""" if isinstance(item, Integral): start = 0 if item == 0 else self.split_indices[item - 1] end = self.split_indices[item] return CoordNeighborList( self.geometry, self.xyzs[item], self._finder_results[start:end], ) else: raise ValueError("Only integer indexing is supported.") @set_module("sisl.geom") class CoordNeighborList(Neighbors): """List of atoms that are close to a coordinate in space. The usual way to get a `CoordNeighborList` object is by iterating over a `CoordsNeighborList`. See its documentation for examples. See Also -------- CoordsNeighborList The list that, when iterated, returns `CoordNeighborList` objects. """ xyz: np.ndarray """The Cartesian coordinate for which the neighbors are stored.""" def __init__(self, geometry: Geometry, xyz: np.ndarray, finder_results: np.ndarray): self.xyz = xyz super().__init__(geometry, finder_results)
[docs] @cached_property def n_neighbors(self) -> int: """Number of neighbors of the coordinate.""" return len(self._finder_results)