File: force_directed.py

package info (click to toggle)
python-vispy 0.14.3-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 8,840 kB
  • sloc: python: 59,436; javascript: 6,800; makefile: 69; sh: 6
file content (211 lines) | stat: -rw-r--r-- 7,500 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# -*- coding: utf-8 -*-
# Copyright (c) Vispy Development Team. All Rights Reserved.
# Distributed under the (new) BSD License. See LICENSE.txt for more info.
"""
Force-Directed Graph Layout
===========================

This module contains implementations for a force-directed layout, where the
graph is modelled like a collection of springs or as a collection of
particles attracting and repelling each other. The whole graph tries to
reach a state which requires the minimum energy.
"""

import numpy as np

try:
    from scipy.sparse import issparse
except ImportError:
    def issparse(*args, **kwargs):
        return False

from ..util import _straight_line_vertices, _rescale_layout


class fruchterman_reingold(object):
    r"""Fruchterman-Reingold implementation adapted from NetworkX.

    In the Fruchterman-Reingold algorithm, the whole graph is modelled as a
    collection of particles, it runs a simplified particle simulation to
    find a nice layout for the graph.

    Parameters
    ----------
    optimal : number
        Optimal distance between nodes. Defaults to :math:`1/\\sqrt{N}` where
        N is the number of nodes.
    iterations : int
        Number of iterations to perform for layout calculation.
    pos : array
        Initial positions of the nodes

    Notes
    -----
    The algorithm is explained in more detail in the original paper [1]_.

    .. [1] Fruchterman, Thomas MJ, and Edward M. Reingold. "Graph drawing by
       force-directed placement." Softw., Pract. Exper. 21.11 (1991),
       1129-1164.
    """

    def __init__(self, optimal=None, iterations=50, pos=None):
        self.dim = 2
        self.optimal = optimal
        self.iterations = iterations
        self.num_nodes = None
        self.pos = pos

    def __call__(self, adjacency_mat, directed=False):
        """
        Starts the calculation of the graph layout.

        This is a generator, and after each iteration it yields the new
        positions for the nodes, together with the vertices for the edges
        and the arrows.

        There are two solvers here: one specially adapted for SciPy sparse
        matrices, and the other for larger networks.

        Parameters
        ----------
        adjacency_mat : array
            The graph adjacency matrix.
        directed : bool
            Wether the graph is directed or not. If this is True,
            it will draw arrows for directed edges.

        Yields
        ------
        layout : tuple
            For each iteration of the layout calculation it yields a tuple
            containing (node_vertices, line_vertices, arrow_vertices). These
            vertices can be passed to the `MarkersVisual` and `ArrowVisual`.
        """
        if adjacency_mat.shape[0] != adjacency_mat.shape[1]:
            raise ValueError("Adjacency matrix should be square.")

        self.num_nodes = adjacency_mat.shape[0]

        if issparse(adjacency_mat):
            # Use the sparse solver
            solver = self._sparse_fruchterman_reingold
        else:
            solver = self._fruchterman_reingold

        for result in solver(adjacency_mat, directed):
            yield result

    def _fruchterman_reingold(self, adjacency_mat, directed=False):
        if self.optimal is None:
            self.optimal = 1 / np.sqrt(self.num_nodes)

        if self.pos is None:
            # Random initial positions
            pos = np.asarray(
                np.random.random((self.num_nodes, self.dim)),
                dtype=np.float32
            )
        else:
            pos = self.pos.astype(np.float32)

        # Yield initial positions
        line_vertices, arrows = _straight_line_vertices(adjacency_mat, pos,
                                                        directed)
        yield pos, line_vertices, arrows

        # The initial "temperature"  is about .1 of domain area (=1x1)
        # this is the largest step allowed in the dynamics.
        t = 0.1

        # Simple cooling scheme.
        # Linearly step down by dt on each iteration so last iteration is
        # size dt.
        dt = t / float(self.iterations+1)
        # The inscrutable (but fast) version
        # This is still O(V^2)
        # Could use multilevel methods to speed this up significantly
        for iteration in range(self.iterations):
            delta_pos = _calculate_delta_pos(adjacency_mat, pos, t,
                                             self.optimal)
            pos += delta_pos
            _rescale_layout(pos)

            # cool temperature
            t -= dt

            # Calculate edge vertices and arrows
            line_vertices, arrows = _straight_line_vertices(adjacency_mat,
                                                            pos, directed)

            yield pos, line_vertices, arrows

    def _sparse_fruchterman_reingold(self, adjacency_mat, directed=False):
        # Optimal distance between nodes
        if self.optimal is None:
            self.optimal = 1 / np.sqrt(self.num_nodes)

        # Change to list of list format
        # Also construct the matrix in COO format for easy edge construction
        adjacency_arr = adjacency_mat.toarray()
        adjacency_coo = adjacency_mat.tocoo()

        if self.pos is None:
            # Random initial positions
            pos = np.asarray(
                np.random.random((self.num_nodes, self.dim)),
                dtype=np.float32
            )
        else:
            pos = self.pos.astype(np.float32)

        # Yield initial positions
        line_vertices, arrows = _straight_line_vertices(adjacency_coo, pos,
                                                        directed)
        yield pos, line_vertices, arrows

        # The initial "temperature"  is about .1 of domain area (=1x1)
        # This is the largest step allowed in the dynamics.
        t = 0.1
        # Simple cooling scheme.
        # Linearly step down by dt on each iteration so last iteration is
        # size dt.
        dt = t / float(self.iterations+1)
        for iteration in range(self.iterations):
            delta_pos = _calculate_delta_pos(adjacency_arr, pos, t,
                                             self.optimal)
            pos += delta_pos
            _rescale_layout(pos)

            # Cool temperature
            t -= dt

            # Calculate line vertices
            line_vertices, arrows = _straight_line_vertices(adjacency_coo,
                                                            pos, directed)

            yield pos, line_vertices, arrows


def _calculate_delta_pos(adjacency_arr, pos, t, optimal):
    """Helper to calculate the delta position"""
    # XXX eventually this should be refactored for the sparse case to only
    # do the necessary pairwise distances
    delta = pos[:, np.newaxis, :] - pos

    # Distance between points
    distance2 = (delta*delta).sum(axis=-1)
    # Enforce minimum distance of 0.01
    distance2 = np.where(distance2 < 0.0001, 0.0001, distance2)
    distance = np.sqrt(distance2)
    # Displacement "force"
    displacement = np.zeros((len(delta), 2))
    for ii in range(2):
        displacement[:, ii] = (
            delta[:, :, ii] *
            ((optimal * optimal) / (distance*distance) -
             (adjacency_arr * distance) / optimal)).sum(axis=1)

    length = np.sqrt((displacement**2).sum(axis=1))
    length = np.where(length < 0.01, 0.1, length)
    delta_pos = displacement * t / length[:, np.newaxis]
    return delta_pos