# Copyright (c) 2010-2025 The Regents of the University of Michigan
# This file is from the freud project, released under the BSD 3-Clause License.
r"""
The :mod:`freud.locality` module contains data structures to efficiently
locate points based on their proximity to other points.
"""
import inspect
import numpy as np
import freud._locality
import freud.box
import freud.util
from freud._util import ( # noqa F401
ManagedArray_double,
ManagedArray_float,
ManagedArray_unsignedint,
ManagedArrayVec3_float,
Vector_double,
Vector_float,
Vector_unsignedint,
VectorVec3_float,
)
from freud.errors import NO_DEFAULT_QUERY_ARGS_MESSAGE
from freud.util import _Compute
ITERATOR_TERMINATOR = freud._locality.get_iterator_terminator()
class _QueryArgs:
r"""Container for query arguments.
This class is use internally throughout freud to provide a nice interface
between keyword- or dict-style query arguments and the C++ QueryArgs
object. All arguments are funneled through this interface, which constructs
the appropriate C++ QueryArgs object that can then be used in C++ compute
calls.
"""
def __init__(
self,
mode=None,
r_min=None,
r_max=None,
r_guess=None,
num_neighbors=None,
exclude_ii=None,
scale=None,
**kwargs,
):
self._cpp_obj = freud._locality.QueryArgs()
self.mode = mode
if r_max is not None:
self.r_max = r_max
if r_min is not None:
self.r_min = r_min
if r_guess is not None:
self.r_guess = r_guess
if num_neighbors is not None:
self.num_neighbors = num_neighbors
if exclude_ii is not None:
self.exclude_ii = exclude_ii
if scale is not None:
self.scale = scale
if len(kwargs):
err_str = ", ".join(f"{k} = {v}" for k, v in kwargs.items())
raise ValueError(
"The following invalid query arguments were provided: " + err_str
)
def update(self, qargs):
if qargs is None:
return
for arg in qargs:
if hasattr(self, arg):
setattr(self, arg, qargs[arg])
else:
msg = "An invalid query argument was provided."
raise ValueError(msg)
@classmethod
def from_dict(cls, mapping):
"""Create _QueryArgs from mapping."""
return cls(**mapping)
@property
def mode(self):
if self._cpp_obj.mode == freud._locality.QueryType.none:
return None
if self._cpp_obj.mode == freud._locality.QueryType.ball:
return "ball"
if self._cpp_obj.mode == freud._locality.QueryType.nearest:
return "nearest"
msg = f"Unknown mode {self._cpp_obj.mode} set!"
raise ValueError(msg)
@mode.setter
def mode(self, value):
if value == "none" or value is None:
self._cpp_obj.mode = freud._locality.QueryType.none
elif value == "ball":
self._cpp_obj.mode = freud._locality.QueryType.ball
elif value == "nearest":
self._cpp_obj.mode = freud._locality.QueryType.nearest
else:
msg = "An invalid mode was provided."
raise ValueError(msg)
@property
def r_guess(self):
return self._cpp_obj.r_guess
@r_guess.setter
def r_guess(self, value):
self._cpp_obj.r_guess = value
@property
def r_min(self):
return self._cpp_obj.r_min
@r_min.setter
def r_min(self, value):
self._cpp_obj.r_min = value
@property
def r_max(self):
return self._cpp_obj.r_max
@r_max.setter
def r_max(self, value):
self._cpp_obj.r_max = value
@property
def num_neighbors(self):
return self._cpp_obj.num_neighbors
@num_neighbors.setter
def num_neighbors(self, value):
self._cpp_obj.num_neighbors = value
@property
def exclude_ii(self):
return self._cpp_obj.exclude_ii
@exclude_ii.setter
def exclude_ii(self, value):
self._cpp_obj.exclude_ii = value
@property
def scale(self):
return self._cpp_obj.scale
@scale.setter
def scale(self, value):
self._cpp_obj.scale = value
def __repr__(self):
return (
f"freud.locality.{type(self).__name__}"
f"(mode={self.mode}, r_max={self.r_max}, "
f"num_neighbors={self.num_neighbors}, exclude_ii={self.exclude_ii}, "
f"scale={self.scale})"
)
def __str__(self):
return repr(self)
[docs]
class NeighborQueryResult:
r"""Class encapsulating the output of queries of NeighborQuery objects.
.. warning::
This class should not be instantiated directly, it is the
return value of the :meth:`~NeighborQuery.query` method of
:class:`~NeighborQuery`. The class provides a convenient
interface for iterating over query results, and can be
transparently converted into a list or a
:class:`~NeighborList` object.
The :class:`~NeighborQueryResult` makes it easy to work with the results of
queries and convert them to various natural objects. Additionally, the
result is a generator, making it easy for users to lazily iterate over the
object.
"""
def __init__(self, nq, points, query_args):
self._nq = nq
self._points = np.array(points, dtype=np.float32)
self._query_args = query_args
def __iter__(self):
iterator_cpp = self._nq._cpp_obj.query(self._points, self._query_args._cpp_obj)
npoint = iterator_cpp.next()
while npoint != ITERATOR_TERMINATOR:
yield (
npoint.getQueryPointIdx(),
npoint.getPointIdx(),
npoint.getDistance(),
)
npoint = iterator_cpp.next()
[docs]
def toNeighborList(self, sort_by_distance=False):
"""Convert query result to a freud :class:`~NeighborList`.
Args:
sort_by_distance (bool):
If :code:`True`, sort neighboring bonds by distance.
If :code:`False`, sort neighboring bonds by point index
(Default value = :code:`False`).
Returns:
:class:`~NeighborList`: A :class:`~NeighborList` containing all
neighbor pairs found by the query generating this result object.
"""
iterator_cpp = self._nq._cpp_obj.query(self._points, self._query_args._cpp_obj)
nlist_cpp = iterator_cpp.toNeighborList(sort_by_distance)
return _nlist_from_cnlist(nlist_cpp)
[docs]
class NeighborQuery:
r"""Class representing a set of points along with the ability to query for
neighbors of these points.
.. warning::
This class should not be instantiated directly. The subclasses
:class:`~AABBQuery` and :class:`~LinkCell` provide the
intended interfaces.
The :class:`~.NeighborQuery` class represents the abstract interface for
neighbor finding. The class contains a set of points and a simulation box,
the latter of which is used to define the system and the periodic boundary
conditions required for finding neighbors of these points. The primary mode
of interacting with the :class:`~.NeighborQuery` is through the
:meth:`~NeighborQuery.query` and :meth:`~NeighborQuery.queryBall`
functions, which enable finding either the nearest neighbors of a point or
all points within a distance cutoff, respectively. Subclasses of
NeighborQuery implement these methods based on the nature of the underlying
data structure.
Args:
box (:class:`freud.box.Box`):
Simulation box.
points ((:math:`N`, 3) :class:`numpy.ndarray`):
Point coordinates to build the structure.
"""
def __init__(self):
msg = (
"The NeighborQuery class is abstract, and should not be "
"directly instantiated"
)
raise RuntimeError(msg)
[docs]
@classmethod
def from_system(cls, system, dimensions=None):
r"""Create a :class:`~.NeighborQuery` from any system-like object.
The standard concept of a system in **freud** is any object that
provides a way to access a box-like object (anything that can be
coerced to a box by :meth:`freud.box.Box.from_box`) and an array-like
object (according to `NumPy's definition
<https://docs.scipy.org/doc/numpy/user/basics.creation.html#converting-python-array-like-objects-to-numpy-arrays>`_)
of particle positions that turns into a :math:`N\times 3` array.
Supported types for :code:`system` include:
* :class:`~.locality.AABBQuery`
* :class:`~.locality.LinkCell`
* A sequence of :code:`(box, points)` where :code:`box` is a
:class:`~.box.Box` and :code:`points` is a :class:`numpy.ndarray`.
* Objects with attributes :code:`box` and :code:`points`.
* :class:`MDAnalysis.coordinates.timestep.Timestep`
* :class:`gsd.hoomd.Snapshot`
* :class:`gsd.hoomd.Frame`
* :class:`garnett.trajectory.Frame`
* :class:`ovito.data.DataCollection`
* :class:`hoomd.Snapshot`
Args:
system (system-like object):
Any object that can be converted to a :class:`~.NeighborQuery`.
dimensions (int):
Whether the object is 2 or 3 dimensional. It may be inferred if
not provided, but in some cases inference is not possible, in
which case it will default to 3 (Default value = None).
Returns:
:class:`freud.locality.NeighborQuery`:
The same :class:`~.NeighborQuery` object if one is given, or an
instance of :class:`~.NeighborQuery` built from an inferred
:code:`box` and :code:`points`.
"""
def _match_class_path(obj, *matches):
return any(
cls.__module__ + "." + cls.__name__ in matches
for cls in inspect.getmro(type(obj))
)
if isinstance(system, cls):
return system
# MDAnalysis compatibility
# base namespace for mdanalysis<2.3.0
# timestep namespace for mdanalysis>=2.3.0
if _match_class_path(
system,
"MDAnalysis.coordinates.base.Timestep",
"MDAnalysis.coordinates.timestep.Timestep",
):
system = (system.triclinic_dimensions, system.positions)
# GSD and HOOMD-blue 3 snapshot compatibility
elif _match_class_path(
system, "gsd.hoomd.Frame", "gsd.hoomd.Snapshot", "hoomd.snapshot.Snapshot"
):
# Explicitly construct the box to silence warnings from box
# constructor, HOOMD simulations often have Lz=1 for 2D boxes.
box = np.array(system.configuration.box)
if system.configuration.dimensions == 2:
box[[2, 4, 5]] = 0
system = (box, system.particles.position)
# garnett compatibility (garnett >=0.5)
elif _match_class_path(system, "garnett.trajectory.Frame"):
try:
# garnett >= 0.7
position = system.position
except AttributeError:
# garnett < 0.7
position = system.positions
system = (system.box, position)
# OVITO compatibility
elif _match_class_path(
system,
"ovito.data.DataCollection",
"ovito.plugins.PyScript.DataCollection",
"PyScript.DataCollection",
):
box = freud.Box.from_box(
system.cell.matrix[:, :3], dimensions=2 if system.cell.is2D else 3
)
system = (box, system.particles.positions)
# HOOMD-blue 2 snapshot compatibility
elif (
hasattr(system, "box")
and hasattr(system, "particles")
and hasattr(system.particles, "position")
):
# Explicitly construct the box to silence warnings from box
# constructor because HOOMD sets Lz=1 rather than 0 for 2D boxes.
if system.box.dimensions == 2:
box = freud.Box(system.box.Lx, system.box.Ly, xy=system.box.xy)
else:
box = system.box
system = (box, system.particles.position)
# Duck type systems with attributes into a (box, points) tuple
elif hasattr(system, "box") and hasattr(system, "points"):
system = (system.box, system.points)
if cls == NeighborQuery:
# If called from this abstract parent class, always make
# :class:`~._RawPoints`.
return _RawPoints(*system)
# Otherwise, use the current class.
return cls(*system)
@property
def box(self):
""":class:`freud.box.Box`: The box object used by this data
structure."""
return freud.box.BoxFromCPP(self._cpp_obj.getBox())
@property
def points(self):
""":class:`np.ndarray`: The array of points in this data structure."""
return np.asarray(self._points)
[docs]
def query(self, query_points, query_args):
r"""Query for nearest neighbors of the provided point.
Args:
query_points ((:math:`N`, 3) :class:`numpy.ndarray`):
Points to query for.
query_args (dict):
Query arguments determining how to find neighbors. For
information on valid query argument, see the `Query API
<https://freud.readthedocs.io/en/stable/topics/querying.html>`_.
Returns:
:class:`~.NeighborQueryResult`: Results object containing the
output of this query.
"""
query_points = freud.util._convert_array(
np.atleast_2d(query_points), shape=(None, 3)
)
args = _QueryArgs.from_dict(query_args)
return NeighborQueryResult(self, query_points, args)
[docs]
def plot(self, ax=None, title=None, *args, **kwargs):
"""Plot system box and points.
Args:
ax (:class:`matplotlib.axes.Axes`):
Axis to plot on. If :code:`None`, make a new figure and axis.
The axis projection (2D or 3D) must match the dimensionality of
the system (Default value = :code:`None`).
title (str):
Title of the plot (Default value = :code:`None`).
*args:
Passed on to :meth:`mpl_toolkits.mplot3d.Axes3D.plot` or
:meth:`matplotlib.axes.Axes.plot`.
**kwargs:
Passed on to :meth:`mpl_toolkits.mplot3d.Axes3D.plot` or
:meth:`matplotlib.axes.Axes.plot`.
Returns:
tuple (:class:`matplotlib.axes.Axes`, \
:class:`matplotlib.collections.PathCollection`):
Axis and point data for the plot.
"""
import freud.plot
return freud.plot.system_plot(self, ax=ax, title=title, *args, **kwargs) # noqa: B026 - it works
[docs]
class NeighborList:
r"""Class representing bonds between two sets of points.
Compute classes contain a set of bonds between two sets of position
arrays ("query points" and "points") and hold a list of index pairs
:math:`\left(i, j\right)` where
:math:`i < N_{query\_points}, j < N_{points}` corresponding to neighbor
pairs between the two sets.
For efficiency, all bonds must be sorted by the query point index, from
least to greatest. Bonds have an query point index :math:`i` and a point
index :math:`j`. The first bond index corresponding to a given query point
can be found in :math:`\log(N_{bonds})` time using
:meth:`find_first_index`, because bonds are ordered by the query point
index.
.. note::
Typically, there is no need to instantiate this class directly.
In most cases, users should manipulate
:class:`freud.locality.NeighborList` objects received from a
neighbor search algorithm, such as :class:`freud.locality.LinkCell`,
:class:`freud.locality.AABBQuery`, or :class:`freud.locality.Voronoi`.
Also available as ``freud.NeighborList``.
Example::
# Assume we have position as Nx3 array
aq = freud.locality.AABBQuery(box, positions)
nlist = aq.query(positions, {'r_max': 3}).toNeighborList()
# Get all vectors from central particles to their neighbors
rijs = nlist.vectors
rijs = box.wrap(rijs)
The NeighborList can be indexed to access bond particle indices. Example::
for i, j in nlist[:]:
print(i, j)
"""
[docs]
@classmethod
def from_arrays(
cls,
num_query_points,
num_points,
query_point_indices,
point_indices,
vectors,
weights=None,
):
r"""Create a NeighborList from a set of bond information arrays.
Example::
import freud
import numpy as np
box = freud.box.Box(2, 3, 4, 0, 0, 0)
query_points = np.array([[0, 0, 0], [0, 0, 1]])
points = np.array([[0, 0, -1], [0.5, -1, 0]])
num_query_points = len(query_points)
num_points = len(points)
query_point_indices = np.array([0, 0, 1])
point_indices = np.array([0, 1, 1])
vectors = box.wrap(points[point_indices] - query_points[query_point_indices])
nlist = freud.locality.NeighborList.from_arrays(
num_query_points, num_points, query_point_indices,
point_indices, vectors)
Args:
num_query_points (int):
Number of query points (corresponding to
:code:`query_point_indices`).
num_points (int):
Number of points (corresponding to :code:`point_indices`).
query_point_indices (:class:`np.ndarray`):
Array of integers corresponding to indices in the set of
query points.
point_indices (:class:`np.ndarray`):
Array of integers corresponding to indices in the set of
points.
vectors (:math:`\left(N_{bonds}, 3\right)` :class:`numpy.ndarray`):
Array of bond vectors from query points to corresponding
points.
weights (:math:`\left(N_{bonds} \right)` :class:`np.ndarray`, optional):
Array of per-bond weights (if :code:`None` is given, use a
value of 1 for each weight) (Default value = :code:`None`).
""" # noqa 501
query_point_indices = freud.util._convert_array(
query_point_indices, shape=(None,), dtype=np.uint32
).copy()
point_indices = freud.util._convert_array(
point_indices, shape=query_point_indices.shape, dtype=np.uint32
).copy()
vectors = freud.util._convert_array(
vectors, shape=(len(query_point_indices), 3), dtype=np.float32
).copy()
if weights is None:
weights = np.ones(query_point_indices.shape, dtype=np.float32)
weights = freud.util._convert_array(
weights, shape=query_point_indices.shape
).copy()
result = cls()
result._cpp_obj = freud._locality.NeighborList(
query_point_indices,
num_query_points,
point_indices,
num_points,
vectors,
weights,
)
return result
[docs]
@classmethod
def all_pairs(cls, system, query_points=None, exclude_ii=True):
R"""Create a NeighborList where all pairs of points are neighbors.
More explicitly, this method returns a NeighborList in which all pairs of
points :math:`i`, :math:`j` are neighbors. Pairs such that :math:`i = j`
can also be excluded using the ``exclude_ii`` option. The weight of all
neighbors pairs in the returned list will be 1.
Args:
system:
Any object that is valid argument to
:class:`freud.locality.NeighborQuery.from_system`.
query_points ((:math:`N_{query\_points}`, 3) :class:`np.ndarray`, optional):
Query points used to create neighbor pairs. Uses the system's
points if :code:`None` (Default value = :code:`None`).
exclude_ii (bool):
Whether to exclude pairs of particles with the same point index in
the output neighborlist (Default value = ``True``).
"""
nq = NeighborQuery.from_system(system)
box = nq._cpp_obj.getBox()
points = nq.points
if query_points is None:
query_points = points
points = freud.util._convert_array(points, shape=points.shape, dtype=np.float32)
query_points = freud.util._convert_array(
query_points, shape=query_points.shape, dtype=np.float32
)
result = cls()
result._cpp_obj = freud._locality.NeighborList(
points, query_points, box, exclude_ii
)
return result
def __init__(self, _null=False):
self._cpp_obj = None if _null else freud._locality.NeighborList()
[docs]
def get_ptr(self):
r"""Returns a pointer to the raw C++ object we are wrapping."""
return self._cpp_obj
[docs]
def copy_c(self, other):
r"""Copies the contents of other NeighborList into this object."""
self._cpp_obj.copy(other._cpp_obj)
[docs]
def copy(self, other=None):
r"""Create a copy. If other is given, copy its contents into this
object. Otherwise, return a copy of this object.
Args:
other (:class:`freud.locality.NeighborList`, optional):
A NeighborList to copy into this object (Default value =
:code:`None`).
"""
if other is not None:
assert isinstance(other, NeighborList)
self.copy_c(other)
return self
new_copy = NeighborList()
new_copy.copy(self)
return new_copy
def __getitem__(self, key):
r"""Access the bond array by index or slice."""
return self._cpp_obj.getNeighbors().toNumpyArray()[key]
@property
def query_point_indices(self):
"""(:math:`N_{bonds}`) :class:`np.ndarray`: The query point indices for
each bond. This array is read-only to prevent breakage of
:meth:`~.find_first_index()`. Equivalent to indexing with
:code:`[:, 0]`."""
return self[:, 0]
@property
def point_indices(self):
"""(:math:`N_{bonds}`) :class:`np.ndarray`: The point indices for each
bond. This array is read-only to prevent breakage of
:meth:`~.find_first_index()`. Equivalent to indexing with :code:`[:,
1]`."""
return self[:, 1]
@property
def weights(self):
"""(:math:`N_{bonds}`) :class:`np.ndarray`: The weights for each bond.
By default, bonds have a weight of 1."""
return self._cpp_obj.getWeights().toNumpyArray()
@property
def distances(self):
"""(:math:`N_{bonds}`) :class:`np.ndarray`: The distances for each
bond."""
return self._cpp_obj.getDistances().toNumpyArray()
@property
def vectors(self):
r"""(:math:`N_{bonds}`, 3) :class:`np.ndarray`: The vectors for each
bond."""
return self._cpp_obj.getVectors().toNumpyArray()
@property
def segments(self):
"""(:math:`N_{query\\_points}`) :class:`np.ndarray`: A segment array
indicating the first bond index for each query point."""
return self._cpp_obj.getSegments().toNumpyArray()
@property
def neighbor_counts(self):
"""(:math:`N_{query\\_points}`) :class:`np.ndarray`: A neighbor count
array indicating the number of neighbors for each query point."""
return self._cpp_obj.getCounts().toNumpyArray()
def __len__(self):
r"""Returns the number of bonds stored in this object."""
return self._cpp_obj.getNumBonds()
@property
def num_query_points(self):
"""unsigned int: The number of query points.
All query point indices are less than this value.
"""
return self._cpp_obj.getNumQueryPoints()
@property
def num_points(self):
"""unsigned int: The number of points.
All point indices are less than this value.
"""
return self._cpp_obj.getNumPoints()
[docs]
def find_first_index(self, i):
r"""Returns the lowest bond index corresponding to a query particle
with an index :math:`\geq i`.
Args:
i (unsigned int): The particle index.
"""
return self._cpp_obj.find_first_index(i)
[docs]
def filter(self, filt):
r"""Removes bonds that satisfy a boolean criterion.
Args:
filt (:class:`np.ndarray`):
Boolean-like array of bonds to keep (True means the bond
will not be removed).
.. note:: This method modifies this object in-place.
Example::
# Keep only the bonds between particles of type A and type B
nlist.filter(types[nlist.query_point_indices] != types[nlist.point_indices])
""" # E501
filt = np.ascontiguousarray(filt, dtype=bool)
self._cpp_obj.filter(filt)
return self
[docs]
def filter_r(self, r_max, r_min=0):
r"""Removes bonds that are outside of a given radius range.
Args:
r_max (float):
Maximum bond distance in the resulting neighbor list.
r_min (float, optional):
Minimum bond distance in the resulting neighbor list
(Default value = :code:`0`).
"""
self._cpp_obj.filter_r(r_max, r_min)
return self
[docs]
def sort(self, by_distance=False):
r"""Sort the entries in the neighborlist.
Args:
by_distance (bool):
If ``True``, this method sorts the neighborlist entries by
``query_point_index``, then ``distance``, then ``point_index``.
If ``False``, this method sorts the NeighborList entries by
``query_point_index``, then ``point_index``, then ``distance``
(Default value = ``False``).
"""
self._cpp_obj.sort(by_distance)
return self
def _nlist_from_cnlist(c_nlist):
"""Create a Python NeighborList object that points to an existing C++
NeighborList object.
This functions generally serves two purposes. Any special locality
NeighborList generators, like :class:`~.Voronoi`, should use this as a way
to point to the C++ NeighborList they generate internally. Additionally,
any compute method that requires a :class:`~.NeighborList` (i.e. cannot do
with just a :class:`~.NeighborQuery`) should also expose the internally
computed :class:`~.NeighborList` using this method.
Args:
c_nlist (freud._locality.NeighborList):
C++ neighborlist object.
"""
result = NeighborList()
result._cpp_obj = c_nlist
return result
def _make_default_nq(neighbor_query):
r"""Helper function to return a NeighborQuery object.
Currently the resolution for NeighborQuery objects is such that if Python
users pass in a NumPy array of points and a box, we always make a
_RawPoints object. On the C++ side, the _RawPoints object internally
constructs an AABBQuery object to find neighbors if needed. On the Python
side, making the _RawPoints object is just so that compute functions on the
C++ side don't require overloads to work.
Supported types for :code:`neighbor_query` include:
- :class:`~.locality.AABBQuery`
- :class:`~.locality.LinkCell`
- A tuple of :code:`(box, points)` where :code:`box` is a
:class:`~.box.Box` and :code:`points` is a :class:`numpy.ndarray`.
Args:
neighbor_query (:class:`~.locality.NeighborQuery` - like object):
A :class:`~.locality.NeighborQuery` or object that can be
duck-typed into one.
Returns:
:class:`freud.locality.NeighborQuery`
The same :class:`NeighborQuery` object if one is given or
:class:`_RawPoints` built from an inferred :code:`box` and
:code:`points`.
"""
if not isinstance(neighbor_query, NeighborQuery):
nq = _RawPoints(*neighbor_query)
else:
nq = neighbor_query
return nq
def _make_default_nlist(system, neighbors, query_points=None):
r"""Helper function to return a neighbor list object if is given, or to
construct one using AABBQuery if it is not.
Args:
box (:class:`freud.box.Box`):
Simulation box.
points ((:math:`N_{particles}`, 3) :class:`numpy.ndarray`):
Points for the neighborlist.
query_points ((:math:`N_{particles}`, 3) :class:`numpy.ndarray`):
Query points to construct the neighborlist.
query_args (dict):
Query arguments to use. Note that, if it is not one of the provided
query arguments, :code:`exclude_ii` will be set to :code:`False` if
query_points is :code:`None` and :code:`True` otherwise.
nlist (:class:`freud.locality.NeighborList`, optional):
NeighborList to use to find bonds (Default value = :code:`None`).
Returns:
:class:`freud.locality.NeighborList`:
The neighbor list.
"""
if type(neighbors) is NeighborList:
return neighbors
query_args = neighbors.copy()
query_args.setdefault("exclude_ii", query_points is None)
nq = _make_default_nq(system)
qp = query_points if query_points is not None else nq.points
return nq.query(qp, query_args).toNeighborList()
class _RawPoints(NeighborQuery):
r"""Class containing :class:`~.box.Box` and points with no spatial data
structures for accelerating neighbor queries."""
def __init__(self, box, points):
# Assume valid set of arguments is passed
b = freud.util._convert_box(box)
self._points = freud.util._convert_array(
points, shape=(None, 3), dtype=np.float32
).copy()
self._cpp_obj = freud._locality.RawPoints(b._cpp_obj, self._points)
[docs]
class AABBQuery(NeighborQuery):
r"""Use an Axis-Aligned Bounding Box (AABB) tree :cite:`howard2016` to
find neighbors.
Also available as ``freud.AABBQuery``.
Args:
box (:class:`freud.box.Box`):
Simulation box.
points ((:math:`N`, 3) :class:`numpy.ndarray`):
The points to use to build the tree.
"""
def __init__(self, box, points):
# Assume valid set of arguments is passed
b = freud.util._convert_box(box)
self._points = freud.util._convert_array(points, shape=(None, 3)).copy()
self._cpp_obj = freud._locality.AABBQuery(b._cpp_obj, self._points)
[docs]
class LinkCell(NeighborQuery):
r"""Supports efficiently finding all points in a set within a certain
distance from a given point.
Also available as ``freud.LinkCell``.
Args:
box (:class:`freud.box.Box`):
Simulation box.
points ((:math:`N`, 3) :class:`numpy.ndarray`):
The points to bin into the cell list.
cell_width (float, optional):
Width of cells. If not provided, :class:`~.LinkCell` will
estimate a cell width based on the number of points and the box
size, assuming a constant density of points in the box.
"""
def __init__(self, box, points, cell_width=0):
b = freud.util._convert_box(box)
self._points = freud.util._convert_array(points, shape=(None, 3)).copy()
self._cpp_obj = freud._locality.LinkCell(b._cpp_obj, self._points, cell_width)
@property
def cell_width(self):
"""float: Cell width."""
return self._cpp_obj.getCellWidth()
class _PairCompute(_Compute):
r"""Parent class for all compute classes in freud that depend on finding
nearest neighbors.
The purpose of this class is to consolidate some of the logic for parsing
the numerous possible inputs to the compute calls of such classes. In
particular, this class contains a helper function that calls the necessary
functions to create NeighborQuery and NeighborList classes as needed, as
well as dealing with boxes and query arguments.
"""
def _preprocess_arguments(self, system, query_points=None, neighbors=None):
r"""Process standard compute arguments into freud's internal types by
calling all the required internal functions.
This function handles the preprocessing of boxes and points into
:class:`freud.locality.NeighborQuery` objects, the determination of how
to handle the NeighborList object, the creation of default query
arguments as needed, deciding what `query_points` are, and setting the
appropriate `exclude_ii` flag.
Args:
system (:class:`freud.locality.NeighborQuery` or tuple):
If a tuple, must be of the form (box_like, array_like), i.e. it
must be an object that can be converted into a
:class:`freud.locality.NeighborQuery`.
query_points ((:math:`N_{query\_points}`, 3) :class:`numpy.ndarray`, optional):
Query points for preprocessing. Uses :code:`points` if
:code:`None` (Default value = :code:`None`).
neighbors (:class:`freud.locality.NeighborList` or :class:`dict`, optional):
:class:`~.locality.NeighborList` or dictionary of query
arguments to use to find bonds (Default value = :code:`None`).
""" # noqa E501
nq = NeighborQuery.from_system(system)
# Resolve the two possible ways of passing neighbors (query arguments
# or neighbor lists) based on the type of the neighbors argument.
nlist, qargs = self._resolve_neighbors(neighbors, query_points)
if query_points is None:
query_points = nq.points
else:
query_points = freud.util._convert_array(query_points, shape=(None, 3))
num_query_points = query_points.shape[0]
return (nq, nlist, qargs, query_points, num_query_points)
def _resolve_neighbors(self, neighbors, query_points=None):
if type(neighbors) is NeighborList:
nlist = neighbors
qargs = _QueryArgs()
elif neighbors is None or type(neighbors) is dict:
# The default_query_args property must raise a NotImplementedError
# if no query arguments were passed in and the class has no
# reasonable choice of defaults.
try:
query_args = (
self.default_query_args if neighbors is None else neighbors.copy()
)
query_args.setdefault("exclude_ii", query_points is None)
qargs = _QueryArgs.from_dict(query_args)
nlist = NeighborList(_null=True)
except NotImplementedError:
raise
else:
msg = (
"An invalid value was provided for neighbors, "
"which must be a dict or NeighborList object."
)
raise ValueError(msg)
return nlist, qargs
@property
def default_query_args(self):
"""No default query arguments."""
raise NotImplementedError(
NO_DEFAULT_QUERY_ARGS_MESSAGE.format(type(self).__name__)
)
class _SpatialHistogram(_PairCompute):
r"""Parent class for all compute classes in freud that perform a spatial
binning of particle bonds by distance.
"""
def __init__(self):
# Abstract class
pass
@property
def default_query_args(self):
"""The default query arguments are
:code:`{'mode': 'ball', 'r_max': self.r_max}`."""
return dict(mode="ball", r_max=self.r_max)
@_Compute._computed_property
def box(self):
""":class:`freud.box.Box`: The box object used in the last
computation."""
return freud.box.BoxFromCPP(self._cpp_obj.getBox())
@_Compute._computed_property
def bin_counts(self):
""":math:`\\left(N_0, N_1 \\right)` :class:`numpy.ndarray`: The bin counts in the histogram.""" # noqa: E501
return self._cpp_obj.getBinCounts().toNumpyArray()
@property
def bin_centers(self):
""":class:`list` (:class:`numpy.ndarray`): The centers of each bin in the
histogram (has the same shape in each dimension as the histogram itself)."""
centers = self._cpp_obj.getBinCenters()
return [np.array(c) for c in centers]
@property
def bin_edges(self):
""":class:`list` (:class:`numpy.ndarray`): The edges of each bin in the
histogram (is one element larger in each dimension than the histogram
because each bin has a lower and upper bound)."""
edges = self._cpp_obj.getBinEdges()
return [np.array(e) for e in edges]
@property
def bounds(self):
""":class:`list` (:class:`tuple`): A list of tuples indicating upper and
lower bounds of each axis of the histogram."""
return self._cpp_obj.getBounds() # this returns from cpp with the right type
@property
def nbins(self):
""":class:`list`: The number of bins in each dimension of the
histogram."""
return self._cpp_obj.getAxisSizes()
def _reset(self):
# Resets the values of SpatialHistogram in memory.
self._cpp_obj.reset()
class _SpatialHistogram1D(_SpatialHistogram):
r"""Subclasses _SpatialHistogram to provide a simplified API for
properties of 1-dimensional histograms.
"""
def __init__(self):
# Abstract class
pass
@property
def bin_centers(self):
""":math:`(N_{bins}, )` :class:`numpy.ndarray`: The centers of each bin
in the histogram."""
return np.array(self._cpp_obj.getBinCenters()[0])
@property
def bin_edges(self):
""":math:`(N_{bins}+1, )` :class:`numpy.ndarray`: The edges of each bin
in the histogram. It is one element larger because each bin has a lower
and an upper bound."""
return np.array(self._cpp_obj.getBinEdges()[0])
@property
def bounds(self):
"""tuple: A tuple indicating upper and lower bounds of the histogram."""
return self._cpp_obj.getBounds()[0]
@property
def nbins(self):
"""int: The number of bins in the histogram."""
return self._cpp_obj.getAxisSizes()[0]
[docs]
class PeriodicBuffer(_Compute):
r"""Replicate periodic images of points inside a box."""
def __init__(self):
self._cpp_obj = freud._locality.PeriodicBuffer()
[docs]
def compute(self, system, buffer, images=False, include_input_points=False):
r"""Compute the periodic buffer.
Args:
system:
Any object that is a valid argument to
:class:`freud.locality.NeighborQuery.from_system`.
buffer (float or list of 3 floats):
Buffer distance for replication outside the box.
images (bool, optional):
If ``False``, ``buffer`` is a distance. If ``True``,
``buffer`` is a number of images to replicate in each
dimension. Note that one image adds half of a box length to
each side, meaning that one image doubles the box side lengths,
two images triples the box side lengths, and so on.
(Default value = :code:`False`).
include_input_points (bool, optional):
Whether the original points provided by ``system`` are
included in the buffer, (Default value = :code:`False`).
"""
nq = _make_default_nq(system)
if np.ndim(buffer) == 0:
# catches more cases than np.isscalar
buffer_vec = [buffer, buffer, buffer]
elif len(buffer) == 3:
buffer_vec = [buffer[0], buffer[1], buffer[2]]
else:
msg = "buffer must be a scalar or have length 3."
raise ValueError(msg)
self._cpp_obj.compute(nq._cpp_obj, buffer_vec, images, include_input_points)
return self
@_Compute._computed_property
def buffer_points(self):
""":math:`\\left(N_{buffer}, 3\\right)` :class:`numpy.ndarray`: The
buffer point positions."""
return self._cpp_obj.getBufferPoints().toNumpyArray()
# return np.asarray([[p.x, p.y, p.z] for p in points])
@_Compute._computed_property
def buffer_ids(self):
""":math:`\\left(N_{buffer}\\right)` :class:`numpy.ndarray`: The buffer
point ids."""
return self._cpp_obj.getBufferIds().toNumpyArray()
@_Compute._computed_property
def buffer_box(self):
""":class:`freud.box.Box`: The buffer box, expanded to hold the
replicated points."""
return freud.box.BoxFromCPP(self._cpp_obj.getBufferBox())
def __repr__(self):
return f"freud.locality.{type(self).__name__}()"
def __str__(self):
return repr(self)
[docs]
class Voronoi(_Compute):
r"""Computes Voronoi diagrams using voro++.
Voronoi diagrams (`Wikipedia
<https://en.wikipedia.org/wiki/Voronoi_diagram>`_) are composed of convex
polytopes (polyhedra in 3D, polygons in 2D) called cells, corresponding to
each input point. The cells bound a region of Euclidean space for which all
contained points are closer to a corresponding input point than any other
input point. A ridge is defined as a boundary between cells, which contains
points equally close to two or more input points.
The voro++ library :cite:`Rycroft2009` is used for fast computations of the
Voronoi diagram.
"""
def __init__(self):
self._cpp_obj = freud._locality.Voronoi()
[docs]
def compute(self, system):
r"""Compute Voronoi diagram.
Args:
system:
Any object that is a valid argument to
:class:`freud.locality.NeighborQuery.from_system`.
"""
nq = NeighborQuery.from_system(system)
self._cpp_obj.compute(nq._cpp_obj)
self._box = nq.box
return self
@_Compute._computed_property
def polytopes(self):
"""list[:class:`numpy.ndarray`]: A list of :class:`numpy.ndarray`
defining Voronoi polytope vertices for each cell."""
polytopes = []
raw_polytopes = self._cpp_obj.getPolytopes()
for i in range(len(raw_polytopes)):
raw_vertices = raw_polytopes[i]
num_verts = len(raw_vertices)
polytope_vertices = np.empty((num_verts, 3), dtype=np.float64)
for j in range(num_verts):
polytope_vertices[j, 0] = raw_vertices[j][0]
polytope_vertices[j, 1] = raw_vertices[j][1]
polytope_vertices[j, 2] = raw_vertices[j][2]
polytopes.append(np.asarray(polytope_vertices))
return polytopes
@_Compute._computed_property
def volumes(self):
""":math:`\\left(N_{points} \\right)` :class:`numpy.ndarray`: Returns
an array of Voronoi cell volumes (areas in 2D)."""
return self._cpp_obj.getVolumes().toNumpyArray()
@_Compute._computed_property
def nlist(self):
r"""Returns the computed :class:`~.locality.NeighborList`.
The :class:`~.locality.NeighborList` computed by this class is
weighted. In 2D systems, the bond weight is the length of the ridge
(boundary line) between the neighboring points' Voronoi cells. In 3D
systems, the bond weight is the area of the ridge (boundary polygon)
between the neighboring points' Voronoi cells. The weights are not
normalized, and the weights for each query point sum to the surface
area (perimeter in 2D) of the polytope.
It is possible for pairs of points to appear multiple times in the
neighbor list. For example, in a small unit cell, points may neighbor
one another on multiple sides because of periodic boundary conditions.
Returns:
:class:`~.locality.NeighborList`: Neighbor list.
"""
return _nlist_from_cnlist(self._cpp_obj.getNeighborList())
def __repr__(self):
return f"freud.locality.{type(self).__name__}()"
def __str__(self):
return repr(self)
[docs]
def plot(self, ax=None, color_by=None, cmap=None):
"""Plot Voronoi diagram.
Args:
ax (:class:`matplotlib.axes.Axes`): Axis to plot on. If
:code:`None`, make a new figure and axis.
(Default value = :code:`None`)
color_by (bool):
If :code:`'sides'`, color cells by the number of sides.
If :code:`'area'`, color cells by their area.
If :code:`None`, random colors are used for each cell.
(Default value = :code:`None`)
cmap (str):
Colormap name to use (Default value = :code:`None`).
Returns:
:class:`matplotlib.axes.Axes`: Axis with the plot.
"""
import freud.plot
if not self._box.is2D:
return None
return freud.plot.voronoi_plot(self, self._box, ax, color_by, cmap)
def _repr_png_(self):
try:
import freud.plot
return freud.plot._ax_to_bytes(self.plot())
except (AttributeError, ImportError):
return None
[docs]
class Filter(_PairCompute):
"""Filter an Existing :class:`.NeighborList`.
This class serves as the base class for all NeighborList filtering methods
in **freud**. Filtering a :class:`.NeighborList` requires first computing
the unfiltered :class:`.NeighborList` from a system and a set of query
arguments. Then, based on the arrangement of particles, their shapes, and
other criteria determined by the derived class, some of the neighbors are
removed from the unfiltered :class:`.NeighborList`.
The compute method of each :class:`.Filter` class will take a system object
along with a neighbors dictionary specifying query arguments. The
``neighbors`` dictionary along with the system object will be used to build
the unfiltered neighborlist, which will then be filtered according to the
filter class. After the calculation, the filtered neighborlist will be
available as the property ``filtered_nlist`` .
Warning:
This class is abstract and should not be instantiated directly.
"""
def __init__(self):
msg = "The Filter class is abstract and should not be instantiated directly."
raise RuntimeError(msg)
def _preprocess_arguments(self, system, query_points=None, neighbors=None):
"""Use a full neighborlist if neighbors=None."""
nq = NeighborQuery.from_system(system)
if neighbors is None:
neighbors = NeighborList.all_pairs(nq, query_points, query_points is None)
return super()._preprocess_arguments(nq, query_points, neighbors)
[docs]
def compute(self, system, neighbors=None, query_points=None):
r"""Filter a :class:`.Neighborlist`.
Args:
system:
Any object that is a valid argument to
:class:`freud.locality.NeighborQuery.from_system`.
neighbors (:class:`freud.locality.NeighborList` or dict, optional):
Either a :class:`NeighborList` of neighbor pairs to use for the
unfiltered neighbor list, or a dictionary of `query arguments
<https://freud.readthedocs.io/en/stable/topics/querying.html>`__.
If ``None``, an unfiltered neighborlist will be created such that
all pairs of particles are neighbors via :meth:`.NeighborList.all_pairs`
(Default value = ``None``).
query_points ((:math:`N_{query\_points}`, 3) :class:`np.ndarray`, optional):
Query points used to calculate the unfiltered neighborlist. Uses
the system's points if :code:`None` (Default value = :code:`None`).
"""
nq, nlist, qargs, query_points, num_query_points = self._preprocess_arguments(
system, query_points, neighbors
)
self._cpp_obj.compute(nq._cpp_obj, query_points, nlist._cpp_obj, qargs._cpp_obj)
return self
@_Compute._computed_property
def filtered_nlist(self):
""":class:`.NeighborList`: The filtered neighbor list."""
return _nlist_from_cnlist(self._cpp_obj.getFilteredNlist())
@_Compute._computed_property
def unfiltered_nlist(self):
""":class:`.NeighborList`: The unfiltered neighbor list."""
return _nlist_from_cnlist(self._cpp_obj.getUnfilteredNlist())
[docs]
class FilterSANN(Filter):
"""Filter a :class:`.NeighborList` via the SANN method.
The Solid Angle Nearest Neighbor (SANN) method :cite:`vanMeel2012` is a
parameter-free algorithm for the identification of nearest neighbors. The
SANN method attributes to each possible neighbor a solid angle and determines
the cutoff radius by the requirement that the sum of the solid angles is 4π.
For performance considerations, SANN is implemented as a way of filtering
a pre-existing set of neighbors due to the high performance cost of sorting
all :math:`N^2` particle pairs by distance. For a more in-depth explanation of
the neighborlist filter concept in **freud**, see :class:`.Filter`.
Warning:
Due to the above design decision, it is possible that the unfiltered
neighborlist will not contain enough neighbors to completely fill the
neighbor shell of some particles in the system. The ``allow_incomplete_shell``
argument to :class:`.FilterSANN`'s constructor controls whether a warning
or exception is raised in these cases.
Note:
The ``filtered_nlist`` computed by this class will be sorted by distance.
Note:
We recommend using unfiltered neighborlists in which no particles are their
own neighbor.
Args:
allow_incomplete_shell (bool):
Whether particles with incomplete neighbor shells are allowed in the
filtered neighborlist. If True, a warning will be raised if there are
particles with incomplete neighbors shells in the filtered neighborlist.
If False, an exception will be raised in the same case (Default value =
:code:`False`).
"""
def __init__(self, allow_incomplete_shell=False):
self._cpp_obj = freud._locality.FilterSANN(allow_incomplete_shell)
[docs]
class FilterRAD(Filter):
"""Filter a :class:`.NeighborList` via the RAD method.
The Relative Angular Distance (RAD) method :cite:`Higham2016` is a parameter-free
algorithm for the identification of nearest neighbors. A particle's neighbor shell
is taken to be all particles that are not blocked by any other particle.
The :class:`.FilterRAD` algorithm considers the potential neighbors of a query point
:math:`i` going radially outward, and filters the neighbors :math:`j` of :math:`i`
which are blocked by a closer neighbor :math:`k`. The RAD algorithm may filter
out all further neighbors of :math:`i` as soon as blocked neighbor :math:`j` is
found. This is the mode corresponding to ``terminate_after_blocked=True`` and is
called "RAD-closed" in :cite:`Higham2016`. If ``terminate_after_blocked=False``,
then :class:`.FilterRAD` will continue to consider neighbors further away than
:math:`j`, only filtering them if they are blocked by a closer neighbor. This mode
is called "RAD-open" in :cite:`Higham2016`.
RAD is implemented as a filter for pre-existing sets of neighbors due to
the high performance cost of sorting all :math:`N^2` particle pairs by
distance. For a more in-depth explanation of the neighborlist filter
concept in **freud**, see :class:`.Filter`.
Warning:
Due to the above design decision, it is possible that the unfiltered
neighborlist will not contain enough neighbors to completely fill the
neighbor shell of some particles in the system. The ``allow_incomplete_shell``
argument to :class:`.FilterRAD`'s constructor controls whether a warning
or exception is raised in these cases.
Note:
The ``filtered_nlist`` computed by this class will be sorted by distance.
Note:
We recommend using unfiltered neighborlists in which no particles are their
own neighbor.
Args:
allow_incomplete_shell (bool):
Whether particles with incomplete neighbor shells are allowed in the
filtered neighborlist. If True, a warning will be raised if there are
particles with incomplete neighbors shells in the filtered neighborlist.
If False, an exception will be raised in the same case. Only considered
when ``terminate_after_blocked=True`` (Default value = :code:`False`).
terminate_after_blocked (bool):
Filter potential neighbors after a closer blocked particle is found
(Default value = :code:`False`).
"""
def __init__(self, allow_incomplete_shell=False, terminate_after_blocked=True):
self._cpp_obj = freud._locality.FilterRAD(
allow_incomplete_shell, terminate_after_blocked
)