Source code for spyrmsd.graph

import importlib.util
import warnings

import numpy as np

from spyrmsd import constants

_supported_backends = ("graph_tool", "networkx", "rustworkx")

_available_backends = []
_current_backend = None

_backend_to_alias = {
    "graph_tool": ["graph_tool", "graphtool", "graph-tool", "graph tool", "gt"],
    "networkx": ["networkx", "nx"],
    "rustworkx": ["rustworkx", "rx"],
}

_alias_to_backend = {}
for backend, aliases in _backend_to_alias.items():
    for alias in aliases:
        _alias_to_backend[alias] = backend


def _dummy(*args, **kwargs):
    """
    Dummy function for backend not set.
    """
    raise NotImplementedError("No backend is set for spyrmsd.")


cycle = _dummy
graph_from_adjacency_matrix = _dummy
lattice = _dummy
match_graphs = _dummy
num_edges = _dummy
num_vertices = _dummy
vertex_property = _dummy

# Check which supported backend is available
for backend in _supported_backends:
    if importlib.util.find_spec(backend) is not None:
        _available_backends.append(backend)


def _validate_backend(backend):
    """
    Validate backend.

    Check if a backend is supported and installed (available).

    Parameters
    ----------
    backend: str
        Backend to validate
    return: str
        Standardized backend name

    Raises
    ------
    ValueError
        If the backend is not recognized or supported
    ImportError
        If the backend is not installed

    Notes
    -----
    This function is case-insensitive.
    """
    standardized_backend = _alias_to_backend.get(backend.lower())

    if standardized_backend is None:
        raise ValueError(f"The {backend} backend is not recognized or supported")

    if standardized_backend not in _available_backends:
        raise ImportError(f"The {backend} backend doesn't seem to be installed")

    return standardized_backend


def _set_backend(backend):
    """
    Set backend to use for graph operations.

    Parameters
    ----------
    backend: str
        Backend to use

    Notes
    -----
    This function sets the :code:`_current_backend` variable with a validated backend.

    This function modifies the global (module) variables.
    """
    # Global (module) variables modified by this function
    global _current_backend
    global cycle, graph_from_adjacency_matrix, lattice, match_graphs, num_edges, num_vertices, vertex_property

    backend = _validate_backend(backend)

    # Check if we actually need to switch backends
    if backend == _current_backend:
        warnings.warn(f"The backend is already {backend}.")
        return

    if backend == "graph_tool":
        from spyrmsd.graphs.gt import cycle as gt_cycle
        from spyrmsd.graphs.gt import (
            graph_from_adjacency_matrix as gt_graph_from_adjacency_matrix,
        )
        from spyrmsd.graphs.gt import lattice as gt_lattice
        from spyrmsd.graphs.gt import match_graphs as gt_match_graphs
        from spyrmsd.graphs.gt import num_edges as gt_num_edges
        from spyrmsd.graphs.gt import num_vertices as gt_num_vertices
        from spyrmsd.graphs.gt import vertex_property as gt_vertex_property

        cycle = gt_cycle
        graph_from_adjacency_matrix = gt_graph_from_adjacency_matrix
        lattice = gt_lattice
        match_graphs = gt_match_graphs
        num_edges = gt_num_edges
        num_vertices = gt_num_vertices
        vertex_property = gt_vertex_property

    elif backend == "networkx":
        from spyrmsd.graphs.nx import cycle as nx_cycle
        from spyrmsd.graphs.nx import (
            graph_from_adjacency_matrix as nx_graph_from_adjacency_matrix,
        )
        from spyrmsd.graphs.nx import lattice as nx_lattice
        from spyrmsd.graphs.nx import match_graphs as nx_match_graphs
        from spyrmsd.graphs.nx import num_edges as nx_num_edges
        from spyrmsd.graphs.nx import num_vertices as nx_num_vertices
        from spyrmsd.graphs.nx import vertex_property as nx_vertex_property

        cycle = nx_cycle
        graph_from_adjacency_matrix = nx_graph_from_adjacency_matrix
        lattice = nx_lattice
        match_graphs = nx_match_graphs
        num_edges = nx_num_edges
        num_vertices = nx_num_vertices
        vertex_property = nx_vertex_property

    elif backend == "rustworkx":
        from spyrmsd.graphs.rx import cycle as rx_cycle
        from spyrmsd.graphs.rx import (
            graph_from_adjacency_matrix as rx_graph_from_adjacency_matrix,
        )
        from spyrmsd.graphs.rx import lattice as rx_lattice
        from spyrmsd.graphs.rx import match_graphs as rx_match_graphs
        from spyrmsd.graphs.rx import num_edges as rx_num_edges
        from spyrmsd.graphs.rx import num_vertices as rx_num_vertices
        from spyrmsd.graphs.rx import vertex_property as rx_vertex_property

        cycle = rx_cycle
        graph_from_adjacency_matrix = rx_graph_from_adjacency_matrix
        lattice = rx_lattice
        match_graphs = rx_match_graphs
        num_edges = rx_num_edges
        num_vertices = rx_num_vertices
        vertex_property = rx_vertex_property

    _current_backend = backend


if len(_available_backends) == 0:
    raise ImportError(
        "No valid backends found. Please ensure that one of the supported backends is installed."
        + f"\nSupported backends: {_supported_backends}"
    )
else:
    if _current_backend is None:
        _set_backend(backend=_available_backends[0])


def _get_backend():
    """
    Get the current backend.
    """
    return _current_backend


[docs]def adjacency_matrix_from_atomic_coordinates( aprops: np.ndarray, coordinates: np.ndarray ) -> np.ndarray: """ Compute adjacency matrix from atomic coordinates. Parameters ---------- aprops: numpy.ndarray Atomic properties coordinates: numpy.ndarray Atomic coordinates Returns ------- numpy.ndarray Adjacency matrix Notes ----- This function is based on an automatic bond perception algorithm: two atoms are considered to be bonded when their distance is smaller than the sum of their covalent radii plus a tolerance value. [3]_ .. warning:: The automatic bond perceptron rule implemented in this functions is very simple and only depends on atomic coordinates. Use with care! .. [3] E. C. Meng and R. A. Lewis, *Determination of molecular topology and atomic hybridization states from heavy atom coordinates*, J. Comp. Chem. **12**, 891-898 (1991). """ n = len(aprops) assert coordinates.shape == (n, 3) A = np.zeros((n, n)) for i in range(n): r_i = constants.anum_to_covalentradius[aprops[i]] for j in range(i + 1, n): r_j = constants.anum_to_covalentradius[aprops[j]] distance = np.sqrt(np.sum((coordinates[i] - coordinates[j]) ** 2)) if distance < (r_i + r_j + constants.connectivity_tolerance): A[i, j] = A[j, i] = 1 return A