Source code for CodeEntropy.levels.search

"""These functions find neighbors.

There are different functions for different types of neighbor searching.
Currently RAD is the default with grid as an alternative.
"""

import MDAnalysis as mda
import numpy as np
from numba import njit


@njit
def _apply_pbc(vec, dimensions, half_dimensions):
    """
    Apply minimum image convention for periodic boundary conditions.

    Args:
        vec (np.ndarray):
            Vector to wrap.
        dimensions (np.ndarray):
            Simulation box dimensions.
        half_dimensions (np.ndarray):
            Half box lengths.

    Returns:
        np.ndarray:
            Wrapped vector.
    """
    for d in range(3):
        if vec[d] > half_dimensions[d]:
            vec[d] -= dimensions[d]
        elif vec[d] < -half_dimensions[d]:
            vec[d] += dimensions[d]
    return vec


@njit
def _rad_blocking_loop(i_coords, sorted_indices, sorted_distances, coms, dimensions):
    """
    Perform RAD neighbor selection using a blocking criterion.

    This is a Numba-compiled implementation of the RAD algorithm, which
    determines whether a molecule j is a neighbor of molecule i by checking
    whether any closer molecule k blocks j based on angular and distance
    relationships.

    The criterion is based on:

        (1 / r_ij)^2 > (1 / r_ik)^2 * cos(theta_jik)

    where k blocks j if the inequality holds.

    Args:
        i_coords (np.ndarray):
            Coordinates of the central molecule.
        sorted_indices (np.ndarray):
            Indices of molecules sorted by distance from i.
        sorted_distances (np.ndarray):
            Distances corresponding to sorted_indices.
        coms (np.ndarray):
            Precomputed center of mass coordinates for all molecules.
        dimensions (np.ndarray):
            Simulation box dimensions for periodic boundary conditions.

    Returns:
        np.ndarray:
            Indices of molecules that belong to the RAD neighbor shell.
    """
    n = sorted_indices.shape[0]
    limit = min(n, 30)

    half_dimensions = 0.5 * dimensions

    inv_r2 = 1.0 / (sorted_distances * sorted_distances)

    shell = np.empty(limit, dtype=np.int64)
    count = 0

    for y in range(limit):
        j_idx = sorted_indices[y]
        r_ij = sorted_distances[y]
        j_coords = coms[j_idx]

        ba = j_coords - i_coords
        ba = _apply_pbc(ba, dimensions, half_dimensions)

        blocked = False

        for z in range(y):
            k_idx = sorted_indices[z]
            r_ik = sorted_distances[z]

            if r_ik > r_ij:
                continue

            k_coords = coms[k_idx]

            ac = k_coords - j_coords
            ac = _apply_pbc(ac, dimensions, half_dimensions)

            dist_ac2 = (ac * ac).sum()

            denom = -2.0 * r_ik * r_ij
            if denom == 0.0:
                continue

            costheta = (dist_ac2 - r_ik * r_ik - r_ij * r_ij) / denom

            if inv_r2[y] < inv_r2[z] * costheta:
                blocked = True
                break

        if not blocked:
            shell[count] = j_idx
            count += 1

    return shell[:count]