Source code for hiperwalk.graph.integer_lattice

from abc import ABC, abstractmethod
import numpy as np
from scipy.sparse import csr_array
from scipy.sparse import eye as sparse_eye
from .graph import Graph
from types import MethodType
from .multigraph import Multigraph
from .weighted_graph import WeightedGraph

def __generate_valid_basis(euc_dim, basis=None):
    if basis is None:
        basis = np.arange(1, euc_dim + 1)

    try:
        basis.shape
    except AttributeError:
        basis = np.array(basis)

    if basis.shape[0] == euc_dim:
        # add negative arrays to basis
        # this explicits the neighbor order
        basis = np.concatenate((basis, -basis))

    if basis.shape[0] != 2*euc_dim:
        raise ValueError("Invalid number of basis vectors. "
                         + str(euc_dim)
                         + " or "
                         + str(2*euc_dim)
                         + " were expected.")

    if len(basis.shape) == 1:
        # generate standard basis
        valid_basis = np.zeros((basis.shape[0], euc_dim), dtype=np.int8)

        for i in range(basis.shape[0]):
            entry = basis[i]
            positive = entry > 0
            entry = entry if positive else -entry
            entry = entry - 1
            valid_basis[i, entry] = 1 if positive else - 1

        basis = valid_basis

    return basis

def __create_adj_matrix(graph):
    num_vert = graph.number_of_vertices()

    indices = [[]]*num_vert
    indptr = np.zeros(num_vert + 1, dtype=np.int32)

    for v in range(num_vert):
        coord = graph.vertex_coordinates(v)

        neigh = np.array([coord + graph._basis[i]
                          for i in range(len(graph._basis))])

        if not graph._periodic:
            adj = [graph._valid_vertex(n, exception=False)
                   for n in neigh]
            neigh = neigh[adj]

        indices[v] = [graph.vertex_number(n) for n in neigh]
        indptr[v + 1] = indptr[v] + len(neigh)

    if graph._periodic:
        indices = np.reshape(indices, indptr[-1])
    else:
        indices = np.array([elem for l in indices for elem in l],
                           dtype=np.int32)
    data = np.ones(indptr[-1], dtype=np.int8)

    adj_matrix = csr_array((data, indices, indptr), copy=False)
    return adj_matrix

def _valid_vertex(self, vertex, exception=False):
    try:
        # coordinates
        if len(vertex) != self._euc_dim:
            if not exception:
                return False
            raise ValueError("Vertex is not a "
                             + str(self._euc_dim)
                             + "-tuple.")

        if self._periodic:
            return True

        for i in range(self._euc_dim):
            if vertex[i] < 0 or vertex[i] >= self._dim[i]:
                if not exception:
                    return False
                raise ValueError("Inexistent vertex"
                                 + str(vertex) + ". "
                                 + "Lattice is not periodic.")

    except TypeError:
        # number
        if vertex < 0 or vertex >= self.number_of_vertices():
            if not exception:
                return False
            raise ValueError("Inexistent vertex " + str(vertex))

    return True

def vertex_number(self, coordinates):
    self._valid_vertex(coordinates, exception=True)
    # number
    try:
        len(coordinates)
    except TypeError:
        return coordinates

    # coordinates
    coordinates = list(coordinates)
    dim = self._dim
    mult = 1
    number = 0
    for i in range(self._euc_dim - 1, -1, -1):
        if self._periodic:
            coordinates[i] = coordinates[i] % self._dim[i]

        number += mult*coordinates[i]
        mult *= dim[i]

    return number

[docs] def vertex_coordinates(self, vertex): r""" Return the coordinates of the given vertex. Given the number of a vertex, return the corresponding coordinates in the integer lattice. Returns ------- :class:`numpy.ndarray` See Also -------- hiperwalk.Graph.vertex_number Notes ----- The vertex number depends on its coordinates and the dimension of the lattice ``dim``. If the coordinates of a vertex are ``(v[0], ..., v[n-1])``, its number is ``v[n-1] + dim[n-1]*v[n-2] + ... + dim[n-1]*...*dim[1]*v[0]``. Examples -------- .. testsetup:: import hiperwalk as hpw The methods ``vertex_coordinates`` is the inverse of ``vertex_number``, and vice versa. .. doctest:: >>> g = hpw.IntegerLattice((3, 3, 3)) >>> tuple(g.vertex_coordinates(0)) (0, 0, 0) >>> g.vertex_number((0, 0, 0)) 0 >>> tuple(g.vertex_coordinates(13)) (1, 1, 1) >>> g.vertex_number((1, 1, 1)) 13 """ self._valid_vertex(vertex, exception=True) dim = self._dim.copy() # coordinates try: len(vertex) if self._periodic: for i in range(self._euc_dim): vertex[i] = vertex[i] % dim[i] return vertex except TypeError: pass # input is number mult = np.prod(dim, dtype=np.int64) coordinates = np.zeros(self._euc_dim, dtype=np.int32) for i in range(self._euc_dim - 1): mult = mult // dim[i] coordinates[i] = vertex // mult # TODO: check which one is more efficient vertex = vertex % mult # vertex -= coordinates[i]*mult coordinates[-1] = vertex return coordinates
[docs] def dimensions(self): r""" Dimensions of integer lattice. Returns ------- dim : tuple of int ``dim[i]`` is the number of vertices in the ``i``-th axis. """ return self._dim
def neighbors(self, vertex): v_num = self.vertex_number(vertex) start = self._adj_matrix.indptr[v_num] end = self._adj_matrix.indptr[v_num + 1] neighs = self._adj_matrix.indices[start:end] if hasattr(vertex, '__iter__'): neighs = np.array([self.vertex_coordinates(n) for n in neighs], dtype=neighs.dtype) return neighs
[docs] def IntegerLattice(dim, basis=None, periodic=True, multiedges=None, weights=None, copy=False): r""" Integer lattice graph. An integer lattice is a lattice in an euclidean space such that every point is a tuple of integers. In the integer lattice graph, every vertex corresponds to a lattice point. Parameters ---------- dim : tuple of int Lattice dimensions where ``dim[i]`` is the number of vertices in the ``i``-th-axis. basis : list of int or matrix, default=None Vectors used to determine the graph adjacency and the corresponding order of neighbors. ``basis`` can be specified in three different ways. Let ``n = len(dim)``. * ``None`` Equivalent to the argument ``[1, ..., n]``. * list of int Adjacency is described by the standard basis where ``i`` corresponds to the array with all entries equal to ``0`` and the ``i-1``-th entry equal to ``1``. Analogously, ``-i`` corresponds to the same array but in the opposite direction (``-1`` instead of ``1``). The values of ``basis`` must satisfy ``1 <= abs(basis) <= n``. Note that 0 is not a valid value because ``-0 == 0``. It is expected that ``len(basis) == 2*n`` or ``len(basis) == n``. If ``len(basis) == n``, the equivalent argument is .. code-block:: python [basis[0], ..., basis[n - 1], -basis[0], ..., -basis[n - 1]] * matrix A matrix with ``2*n`` rows and ``n`` columns. The ``i``-th neighbor of the vertex with coordinates ``(v[0], ..., v[n-1])`` is ``(v[0] + basis[i][0], ..., v[n-1] + basis[i][n-1])``. periodic : bool, default=True ``True`` if the grid has cyclic boundary conditions, ``False`` if it has borders. multiedges, weights: scipy.sparse.csr_array, default=None See :ref:`graph_constructors`. copy : bool, default=False See :ref:`graph_constructors`. Returns ------- :class:`hiperwalk.Graph` See :ref:`graph_constructors` for details. See Also -------- :ref:`graph_constructors`. Notes ----- The **order of neighbors** depends on the value of ``basis``. The vertex number depends on its coordinates and ``dim``. If the coordinates of a vertex are ``(v[0], ..., v[n-1])``, its number is ``v[n-1] + dim[n-1]*v[n-2] + ... + dim[n-1]*...*dim[1]*v[0]``. Examples -------- .. testsetup:: import hiperwalk as hpw .. doctest:: >>> g = hpw.IntegerLattice((3, 3), basis=None) >>> neigh = g.neighbors((1, 1)) >>> [tuple(g.vertex_coordinates(v)) for v in neigh] [(2, 1), (1, 2), (0, 1), (1, 0)] .. doctest:: >>> g = hpw.IntegerLattice((3, 3), basis=[-1, -2]) >>> neigh = g.neighbors((1, 1)) >>> [tuple(g.vertex_coordinates(v)) for v in neigh] [(0, 1), (1, 0), (2, 1), (1, 2)] .. doctest:: >>> basis = [[0, 1], [-1, 1], [1, -1], [0, -1]] >>> g = hpw.IntegerLattice((3, 3), basis=basis) >>> neigh = g.neighbors((1, 1)) >>> [tuple(g.vertex_coordinates(v)) for v in neigh] [(1, 2), (0, 2), (2, 0), (1, 0)] """ if weights is not None and multiedges is not None: raise ValueError( "Both `weights` and `multiedges` arguments were set. " + "Cannot decide whether to create a weighted graph or " + "a multigraph." ) if not hasattr(dim, '__iter__'): dim = [dim] dim = np.array(dim) num_vert = np.prod(dim) # create toy graph g = Graph(sparse_eye(num_vert).tocsr()) # modify toy graph to IntegerLattice g._dim = dim g._periodic = periodic g._euc_dim = len(g._dim) # euclidian space dimension g._num_loops = 0 # use provided (or generated) basis g._basis = __generate_valid_basis(g._euc_dim, basis) # bind methods before updating adjacency matrix g._valid_vertex = MethodType(_valid_vertex, g) g.vertex_number = MethodType(vertex_number, g) g.vertex_coordinates = MethodType(vertex_coordinates, g) g.dimensions = MethodType(dimensions, g) #create adjacency matrix g._set_adj_matrix(__create_adj_matrix(g)) # create multigraph or weighted graph data = weights if weights is not None else multiedges if data is not None: if hasattr(data, 'keys'): data = g._dict_to_adj_matrix(data) copy = False else: g._rearrange_matrix_indices(data) if weights is not None: g2 = WeightedGraph(data, copy=copy) else: g2 = Multigraph(data, copy=copy) g2._dim = g._dim g2._periodic = g._periodic g2._euc_dim = g._euc_dim g2._num_loops = g._num_loops g2._basis = g._basis g2._valid_vertex = MethodType(_valid_vertex, g2) g2.vertex_number = MethodType(vertex_number, g2) g2.vertex_coordinates = MethodType(vertex_coordinates, g2) g2.dimensions = MethodType(dimensions, g2) g = g2 return g