Skip to content

layout

Layout

Bases: object

Default class to create layouts

The Layout class is used to generate node a layout drawer and return the calculated node positions as a dictionary, where the keywords represents the node ids and the values represents a two dimensional tuple with the x and y coordinates for the associated nodes.

Parameters:

Name Type Description Default
nodes list

list with node ids. The list contain a list of unique node ids.

required
**attr dict

Attributes to add to node as key=value pairs. See also layout

{}
See also

layout

Source code in src/pathpyG/visualisations/layout.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
class Layout(object):
    """Default class to create layouts

    The [`Layout`][pathpyG.visualisations.layout.Layout] class is used to generate node a layout drawer and
    return the calculated node positions as a dictionary, where the keywords
    represents the node ids and the values represents a two dimensional tuple
    with the x and y coordinates for the associated nodes.

    Args:
        nodes (list): list with node ids.
            The list contain a list of unique node ids.
        **attr (dict): Attributes to add to node as key=value pairs.
            See also [`layout`][pathpyG.visualisations.layout.layout]

    Note: See also
        [`layout`][pathpyG.visualisations.layout.layout]
    """

    def __init__(self, nodes, adjacency_matrix, **attr):
        """Initialize the Layout class

        The [`Layout`][pathpyG.visualisations.layout.Layout] class is used to generate node a layout drawer and
        return the calculated node positions as a dictionary, where the keywords
        represents the node ids and the values represents a two dimensional tuple
        with the x and y coordinates for the associated nodes.

        Args:
            nodes (list): list with node ids.
                The list contain a list of unique node ids.
            **attr (dict): Attributes to add to node as key=value pairs.
                See also [`layout`][pathpyG.visualisations.layout.layout]
        """

        # initialize variables
        self.nodes = nodes
        self.adjacency_matrix = adjacency_matrix

        # rename the attributes
        attr = self.rename_attributes(**attr)

        # options for the layouts
        self.layout_type = attr.get('layout', None)
        self.k = attr.get('force', None,)
        self.fixed = attr.get('fixed', None)
        self.iterations = attr.get('iterations', 50)
        self.threshold = attr.get('threshold', 1e-4)
        self.weight = attr.get('weight', None)
        self.dimension = attr.get('dimension', 2)
        self.seed = attr.get('seed', None)
        self.positions = attr.get('positions', None)
        self.radius = attr.get('radius', 1.0)
        self.direction = attr.get('direction', 1.0)
        self.start_angle = attr.get('start_angle', 0.0)

        # TODO: allow also higher dimensional layouts
        if self.dimension > 2:
            print('Currently only plots with maximum dimension 2 are supported!')
            self.dimension = 2

    @staticmethod
    def rename_attributes(**kwds):
        """Rename layout attributes.

        In the style dictionary multiple keywords can be used to address
        attributes. These keywords will be converted to an unique key word,
        used in the remaining code.

        | keys | other valid keys |
        | ---- | ---------------- |
        | fixed | `fixed_nodes`, `fixed_vertices`, `fixed_n`, `fixed_v` |
        | positions | `initial_positions`, `node_positions` `vertex_positions`, `n_positions`, `v_positions` |
        """
        names = {'fixed': ['fixed_nodes', 'fixed_vertices',
                           'fixed_v', 'fixed_n'],
                 'positions': ['initial_positions', 'node_positions',
                               'vertex_positions', 'n_positions',
                               'v_positions'],
                 'layout_': ['layout_'],
                 }

        _kwds = {}
        del_keys = []
        for key, value in kwds.items():
            for attr, name_list in names.items():
                for name in name_list:
                    if name in key and name[0] == key[0]:
                        _kwds[key.replace(name, attr).replace(
                            'layout_', '')] = value
                        del_keys.append(key)
                        break
        # remove the replaced keys from the dict
        for key in del_keys:
            del kwds[key]

        return {**_kwds, **kwds}

    def generate_layout(self):
        """Function to pick and generate the right layout."""
        # method names
        names_rand = ['Random', 'random', 'rand', None]
        names_fr = ['Fruchterman-Reingold', 'fruchterman_reingold', 'fr',
                    'spring_layout', 'spring layout', 'FR']
        names_circular = ['circular', 'circle', 'ring', '1d-lattice', 'lattice-1d']
        names_grid = ['grid', '2d-lattice', 'lattice-2d']
        # check which layout should be plotted
        if self.layout_type in names_rand:
            self.layout = self.random()
        elif self.layout_type in names_circular or (self.layout_type == 'lattice' and self.dimension == 1):
            self.layout = self.circular()
        elif self.layout_type in names_grid or (self.layout_type == 'lattice' and self.dimension == 2):
            self.layout = self.grid()
        elif self.layout_type in names_fr:
            self.layout = self.fruchterman_reingold()

        # print(self.layout)
        return self.layout

    def random(self):
        """Position nodes uniformly at random in the unit square.

        For every node, a position is generated by choosing each of dimension
        coordinates uniformly at random on the interval $[0.0, 1.0)$.

        This algorithm can be enabled with the keywords: `Random`,
        `random`, `rand`, or `None`

        Keyword Args:
            dimension (int): Dimension of layout. Currently, only plots in 2 dimension are supported. Defaults to 2.
            seed (int): Set the random state for deterministic node layouts. If int, `seed` is
                the seed used by the random number generator, if None, the a random
                seed by created by the numpy random number generator is used.

        Returns:
            layout (dict): A dictionary of positions keyed by node
        """
        np.random.seed(self.seed)
        layout = np.random.rand(len(self.nodes), self.dimension)
        return dict(zip(self.nodes, layout))

    def fruchterman_reingold(self):
        """Position nodes using Fruchterman-Reingold force-directed algorithm.

        In this algorithm, the nodes are represented by steel rings and the
        edges are springs between them. The attractive force is analogous to the
        spring force and the repulsive force is analogous to the electrical
        force. The basic idea is to minimize the energy of the system by moving
        the nodes and changing the forces between them.

        This algorithm can be enabled with the keywords: `Fruchterman-Reingold`,
        `fruchterman_reingold`, `fr`, `spring_layout`, `spring layout`, `FR`

        Keyword Args:
            force (float): Optimal distance between nodes. If None the distance is set to
                1/sqrt(n) where n is the number of nodes.  Increase this value to move
                nodes farther apart.
            positions (dict): Initial positions for nodes as a dictionary with node as keys and values
                as a coordinate list or tuple.  If None, then use random initial
                positions.
            fixed (list): Nodes to keep fixed at initial position.
            iterations (int): Maximum number of iterations taken. Defaults to 50.
            threshold (float): Threshold for relative error in node position changes.  The iteration
                stops if the error is below this threshold. Defaults to 1e-4.
            weight (string): The edge attribute that holds the numerical value used for the edge
                weight.  If None, then all edge weights are 1.
            dimension (int): Dimension of layout. Currently, only plots in 2 dimension are supported. Defaults to 2.
            seed (int): Set the random state for deterministic node layouts. If int, `seed` is
                the seed used by the random number generator, if None, the a random seed
                by created by the numpy random number generator is used.

        Returns:
            layout (dict): A dictionary of positions keyed by node
        """

        # convert adjacency matrix
        self.adjacency_matrix = self.adjacency_matrix.astype(float)

        if self.fixed is not None:
            self.fixed = np.asarray([self.nodes.index(v) for v in self.fixed])

        if self.positions is not None:
            # Determine size of existing domain to adjust initial positions
            _size = max(coord for t in layout.values() for coord in t) # type: ignore
            if _size == 0:
                _size = 1
            np.random.seed(self.seed)
            self.layout = np.random.rand(
                len(self.nodes), self.dimension) * _size # type: ignore

            for i, n in enumerate(self.nodes):
                if n in self.positions:
                    self.layout[i] = np.asarray(self.positions[n])
        else:
            self.layout = None
            _size = 0

        if self.k is None and self.fixed is not None:
            # We must adjust k by domain size for layouts not near 1x1
            self.k = _size / np.sqrt(len(self.nodes))

        try:
            # Sparse matrix
            if len(self.nodes) < 500:  # sparse solver for large graphs
                raise ValueError
            layout = self._sparse_fruchterman_reingold()
        except:
            layout = self._fruchterman_reingold()

        layout = dict(zip(self.nodes, layout))

        return layout

    def _fruchterman_reingold(self):
        """Fruchterman-Reingold algorithm for dense matrices.

        This algorithm is based on the Fruchterman-Reingold algorithm provided
        by `networkx`. (Copyright (C) 2004-2018 by Aric Hagberg <hagberg@lanl.gov>
        Dan Schult <dschult@colgate.edu> Pieter Swart <swart@lanl.gov> Richard
        Penney <rwpenney@users.sourceforge.net> All rights reserved. BSD
        license.)

        """
        A = self.adjacency_matrix.todense()
        k = self.k
        try:
            _n, _ = A.shape
        except AttributeError:
            print('Fruchterman-Reingold algorithm needs an adjacency matrix as input')
            raise AttributeError

        # make sure we have an array instead of a matrix
        A = np.asarray(A)

        if self.layout is None:
            # random initial positions
            np.random.seed(self.seed)
            layout = np.asarray(np.random.rand(
                _n, self.dimension), dtype=A.dtype)
        else:
            # make sure positions are of same type as matrix
            layout = self.layout.astype(A.dtype) # type: ignore

        # optimal distance between nodes
        if k is None:
            k = np.sqrt(1.0 / _n)
        # the initial "temperature"  is about .1 of domain area (=1x1)
        # this is the largest step allowed in the dynamics.
        # We need to calculate this in case our fixed positions force our domain
        # to be much bigger than 1x1
        t = max(max(layout.T[0]) - min(layout.T[0]),
                max(layout.T[1]) - min(layout.T[1])) * 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)
        delta = np.zeros(
            (layout.shape[0], layout.shape[0], layout.shape[1]), dtype=A.dtype)
        # the inscrutable (but fast) version
        # this is still O(V^2)
        # could use multilevel methods to speed this up significantly
        for iteration in tqdm(range(self.iterations), desc='Calculating Fruchterman-Reingold layout'):
            # matrix of difference between points
            delta = layout[:, np.newaxis, :] - layout[np.newaxis, :, :] # type: ignore
            # distance between points
            distance = np.linalg.norm(delta, axis=-1)
            # enforce minimum distance of 0.01
            np.clip(distance, 0.01, None, out=distance)
            # displacement "force"
            displacement = np.einsum('ijk,ij->ik',
                                     delta,
                                     (k * k / distance**2 - A * distance / k))
            # update layoutitions
            length = np.linalg.norm(displacement, axis=-1)
            length = np.where(length < 0.01, 0.1, length)
            delta_layout = np.einsum('ij,i->ij', displacement, t / length)
            if self.fixed is not None:
                # don't change positions of fixed nodes
                delta_layout[self.fixed] = 0.0
            layout += delta_layout
            # cool temperature
            t -= dt
            error = np.linalg.norm(delta_layout) / _n
            if error < self.threshold:
                break
        return layout

    def _sparse_fruchterman_reingold(self):
        """Fruchterman-Reingold algorithm for sparse matrices.

        This algorithm is based on the Fruchterman-Reingold algorithm provided
        by networkx. (Copyright (C) 2004-2018 by Aric Hagberg <hagberg@lanl.gov>
        Dan Schult <dschult@colgate.edu> Pieter Swart <swart@lanl.gov> Richard
        Penney <rwpenney@users.sourceforge.net> All rights reserved. BSD
        license.)

        """
        A = self.adjacency_matrix
        k = self.k
        try:
            _n, _ = A.shape
        except AttributeError:
            print('Fruchterman-Reingold algorithm needs an adjacency '
                      'matrix as input')
            raise AttributeError
        try:
            from scipy.sparse import spdiags, coo_matrix
        except ImportError:
            print('The sparse Fruchterman-Reingold algorithm needs the '
                      'scipy package: http://scipy.org/')
            raise ImportError
        # make sure we have a LIst of Lists representation
        try:
            A = A.tolil()
        except:
            A = (coo_matrix(A)).tolil()

        if self.layout is None:
            # random initial positions
            np.random.seed(self.seed)
            layout = np.asarray(np.random.rand(
                _n, self.dimension), dtype=A.dtype)
        else:
            # make sure positions are of same type as matrix
            layout = layout.astype(A.dtype) # type: ignore

        # no fixed nodes
        if self.fixed is None:
            self.fixed = []

        # optimal distance between nodes
        if k is None:
            k = np.sqrt(1.0 / _n)
        # the initial "temperature"  is about .1 of domain area (=1x1)
        # this is the largest step allowed in the dynamics.
        t = max(max(layout.T[0]) - min(layout.T[0]),
                max(layout.T[1]) - min(layout.T[1])) * 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)

        displacement = np.zeros((self.dimension, _n))
        for iteration in range(self.iterations):
            displacement *= 0
            # loop over rows
            for i in range(A.shape[0]):
                if i in self.fixed:
                    continue
                # difference between this row's node position and all others
                delta = (layout[i] - layout).T
                # distance between points
                distance = np.sqrt((delta**2).sum(axis=0))
                # enforce minimum distance of 0.01
                distance = np.where(distance < 0.01, 0.01, distance)
                # the adjacency matrix row
                Ai = np.asarray(A.getrowview(i).toarray())
                # displacement "force"
                displacement[:, i] +=\
                    (delta * (k * k / distance**2 - Ai * distance / k)).sum(axis=1)
            # update positions
            length = np.sqrt((displacement**2).sum(axis=0))
            length = np.where(length < 0.01, 0.1, length)
            delta_layout = (displacement * t / length).T
            layout += delta_layout
            # cool temperature
            t -= dt
            err = np.linalg.norm(delta_layout) / _n
            if err < self.threshold:
                break
        return layout


    def circular(self):
        """Position nodes on a circle with given radius.

        This algorithm can be enabled with the keywords: `circular`, `circle`, `ring`, `lattice-1d`, `1d-lattice`, `lattice`

        Keyword Args:
            radius (float): Sets the radius of the circle on which nodes
                are positioned. Defaults to 1.0.
            direction (float): Sets the direction in which nodes are placed on the circle. 1.0 for clockwise (default)
                and -1.0 for counter-clockwise direction. Defaults to 1.0.
            start_angle (float): Sets the angle of the first node relative to the 3pm position on a clock.
                and -1.0 for counter-clockwise direction. Defaults to 90.0.

        Returns:
            layout (dict): A dictionary of positions keyed by node
        """

        n = len(self.nodes)
        rad = 2.0 * np.pi / n
        rotation = (90.0 - self.start_angle*self.direction) * np.pi / 180.0
        layout = {}

        for i in range(n):
            x = self.radius * np.cos(rotation - i*rad*self.direction)
            y = self.radius * np.sin(rotation - i*rad*self.direction)
            layout[self.nodes[i]] = (x,y)

        return layout


    def grid(self):
        """Position nodes on a two-dimensional grid

        This algorithm can be enabled with the keywords: `grid`, `lattice-2d`, `2d-lattice`, `lattice`

        Returns:
            layout (dict): A dictionary of positions keyed by node
        """

        n = len(self.nodes)
        width = 1.0

        # number of nodes in horizontal/vertical direction
        k = np.floor(np.sqrt(n))
        dist = width / k
        layout = {}

        i = 0
        for i in range(n):
            layout[self.nodes[i]] = ((i%k) *dist, -(np.floor(i/k))*dist)
            i += 1

        return layout

__init__

Initialize the Layout class

The Layout class is used to generate node a layout drawer and return the calculated node positions as a dictionary, where the keywords represents the node ids and the values represents a two dimensional tuple with the x and y coordinates for the associated nodes.

Parameters:

Name Type Description Default
nodes list

list with node ids. The list contain a list of unique node ids.

required
**attr dict

Attributes to add to node as key=value pairs. See also layout

{}
Source code in src/pathpyG/visualisations/layout.py
def __init__(self, nodes, adjacency_matrix, **attr):
    """Initialize the Layout class

    The [`Layout`][pathpyG.visualisations.layout.Layout] class is used to generate node a layout drawer and
    return the calculated node positions as a dictionary, where the keywords
    represents the node ids and the values represents a two dimensional tuple
    with the x and y coordinates for the associated nodes.

    Args:
        nodes (list): list with node ids.
            The list contain a list of unique node ids.
        **attr (dict): Attributes to add to node as key=value pairs.
            See also [`layout`][pathpyG.visualisations.layout.layout]
    """

    # initialize variables
    self.nodes = nodes
    self.adjacency_matrix = adjacency_matrix

    # rename the attributes
    attr = self.rename_attributes(**attr)

    # options for the layouts
    self.layout_type = attr.get('layout', None)
    self.k = attr.get('force', None,)
    self.fixed = attr.get('fixed', None)
    self.iterations = attr.get('iterations', 50)
    self.threshold = attr.get('threshold', 1e-4)
    self.weight = attr.get('weight', None)
    self.dimension = attr.get('dimension', 2)
    self.seed = attr.get('seed', None)
    self.positions = attr.get('positions', None)
    self.radius = attr.get('radius', 1.0)
    self.direction = attr.get('direction', 1.0)
    self.start_angle = attr.get('start_angle', 0.0)

    # TODO: allow also higher dimensional layouts
    if self.dimension > 2:
        print('Currently only plots with maximum dimension 2 are supported!')
        self.dimension = 2

circular

Position nodes on a circle with given radius.

This algorithm can be enabled with the keywords: circular, circle, ring, lattice-1d, 1d-lattice, lattice

Other Parameters:

Name Type Description
radius float

Sets the radius of the circle on which nodes are positioned. Defaults to 1.0.

direction float

Sets the direction in which nodes are placed on the circle. 1.0 for clockwise (default) and -1.0 for counter-clockwise direction. Defaults to 1.0.

start_angle float

Sets the angle of the first node relative to the 3pm position on a clock. and -1.0 for counter-clockwise direction. Defaults to 90.0.

Returns:

Name Type Description
layout dict

A dictionary of positions keyed by node

Source code in src/pathpyG/visualisations/layout.py
def circular(self):
    """Position nodes on a circle with given radius.

    This algorithm can be enabled with the keywords: `circular`, `circle`, `ring`, `lattice-1d`, `1d-lattice`, `lattice`

    Keyword Args:
        radius (float): Sets the radius of the circle on which nodes
            are positioned. Defaults to 1.0.
        direction (float): Sets the direction in which nodes are placed on the circle. 1.0 for clockwise (default)
            and -1.0 for counter-clockwise direction. Defaults to 1.0.
        start_angle (float): Sets the angle of the first node relative to the 3pm position on a clock.
            and -1.0 for counter-clockwise direction. Defaults to 90.0.

    Returns:
        layout (dict): A dictionary of positions keyed by node
    """

    n = len(self.nodes)
    rad = 2.0 * np.pi / n
    rotation = (90.0 - self.start_angle*self.direction) * np.pi / 180.0
    layout = {}

    for i in range(n):
        x = self.radius * np.cos(rotation - i*rad*self.direction)
        y = self.radius * np.sin(rotation - i*rad*self.direction)
        layout[self.nodes[i]] = (x,y)

    return layout

fruchterman_reingold

Position nodes using Fruchterman-Reingold force-directed algorithm.

In this algorithm, the nodes are represented by steel rings and the edges are springs between them. The attractive force is analogous to the spring force and the repulsive force is analogous to the electrical force. The basic idea is to minimize the energy of the system by moving the nodes and changing the forces between them.

This algorithm can be enabled with the keywords: Fruchterman-Reingold, fruchterman_reingold, fr, spring_layout, spring layout, FR

Other Parameters:

Name Type Description
force float

Optimal distance between nodes. If None the distance is set to 1/sqrt(n) where n is the number of nodes. Increase this value to move nodes farther apart.

positions dict

Initial positions for nodes as a dictionary with node as keys and values as a coordinate list or tuple. If None, then use random initial positions.

fixed list

Nodes to keep fixed at initial position.

iterations int

Maximum number of iterations taken. Defaults to 50.

threshold float

Threshold for relative error in node position changes. The iteration stops if the error is below this threshold. Defaults to 1e-4.

weight string

The edge attribute that holds the numerical value used for the edge weight. If None, then all edge weights are 1.

dimension int

Dimension of layout. Currently, only plots in 2 dimension are supported. Defaults to 2.

seed int

Set the random state for deterministic node layouts. If int, seed is the seed used by the random number generator, if None, the a random seed by created by the numpy random number generator is used.

Returns:

Name Type Description
layout dict

A dictionary of positions keyed by node

Source code in src/pathpyG/visualisations/layout.py
def fruchterman_reingold(self):
    """Position nodes using Fruchterman-Reingold force-directed algorithm.

    In this algorithm, the nodes are represented by steel rings and the
    edges are springs between them. The attractive force is analogous to the
    spring force and the repulsive force is analogous to the electrical
    force. The basic idea is to minimize the energy of the system by moving
    the nodes and changing the forces between them.

    This algorithm can be enabled with the keywords: `Fruchterman-Reingold`,
    `fruchterman_reingold`, `fr`, `spring_layout`, `spring layout`, `FR`

    Keyword Args:
        force (float): Optimal distance between nodes. If None the distance is set to
            1/sqrt(n) where n is the number of nodes.  Increase this value to move
            nodes farther apart.
        positions (dict): Initial positions for nodes as a dictionary with node as keys and values
            as a coordinate list or tuple.  If None, then use random initial
            positions.
        fixed (list): Nodes to keep fixed at initial position.
        iterations (int): Maximum number of iterations taken. Defaults to 50.
        threshold (float): Threshold for relative error in node position changes.  The iteration
            stops if the error is below this threshold. Defaults to 1e-4.
        weight (string): The edge attribute that holds the numerical value used for the edge
            weight.  If None, then all edge weights are 1.
        dimension (int): Dimension of layout. Currently, only plots in 2 dimension are supported. Defaults to 2.
        seed (int): Set the random state for deterministic node layouts. If int, `seed` is
            the seed used by the random number generator, if None, the a random seed
            by created by the numpy random number generator is used.

    Returns:
        layout (dict): A dictionary of positions keyed by node
    """

    # convert adjacency matrix
    self.adjacency_matrix = self.adjacency_matrix.astype(float)

    if self.fixed is not None:
        self.fixed = np.asarray([self.nodes.index(v) for v in self.fixed])

    if self.positions is not None:
        # Determine size of existing domain to adjust initial positions
        _size = max(coord for t in layout.values() for coord in t) # type: ignore
        if _size == 0:
            _size = 1
        np.random.seed(self.seed)
        self.layout = np.random.rand(
            len(self.nodes), self.dimension) * _size # type: ignore

        for i, n in enumerate(self.nodes):
            if n in self.positions:
                self.layout[i] = np.asarray(self.positions[n])
    else:
        self.layout = None
        _size = 0

    if self.k is None and self.fixed is not None:
        # We must adjust k by domain size for layouts not near 1x1
        self.k = _size / np.sqrt(len(self.nodes))

    try:
        # Sparse matrix
        if len(self.nodes) < 500:  # sparse solver for large graphs
            raise ValueError
        layout = self._sparse_fruchterman_reingold()
    except:
        layout = self._fruchterman_reingold()

    layout = dict(zip(self.nodes, layout))

    return layout

generate_layout

Function to pick and generate the right layout.

Source code in src/pathpyG/visualisations/layout.py
def generate_layout(self):
    """Function to pick and generate the right layout."""
    # method names
    names_rand = ['Random', 'random', 'rand', None]
    names_fr = ['Fruchterman-Reingold', 'fruchterman_reingold', 'fr',
                'spring_layout', 'spring layout', 'FR']
    names_circular = ['circular', 'circle', 'ring', '1d-lattice', 'lattice-1d']
    names_grid = ['grid', '2d-lattice', 'lattice-2d']
    # check which layout should be plotted
    if self.layout_type in names_rand:
        self.layout = self.random()
    elif self.layout_type in names_circular or (self.layout_type == 'lattice' and self.dimension == 1):
        self.layout = self.circular()
    elif self.layout_type in names_grid or (self.layout_type == 'lattice' and self.dimension == 2):
        self.layout = self.grid()
    elif self.layout_type in names_fr:
        self.layout = self.fruchterman_reingold()

    # print(self.layout)
    return self.layout

grid

Position nodes on a two-dimensional grid

This algorithm can be enabled with the keywords: grid, lattice-2d, 2d-lattice, lattice

Returns:

Name Type Description
layout dict

A dictionary of positions keyed by node

Source code in src/pathpyG/visualisations/layout.py
def grid(self):
    """Position nodes on a two-dimensional grid

    This algorithm can be enabled with the keywords: `grid`, `lattice-2d`, `2d-lattice`, `lattice`

    Returns:
        layout (dict): A dictionary of positions keyed by node
    """

    n = len(self.nodes)
    width = 1.0

    # number of nodes in horizontal/vertical direction
    k = np.floor(np.sqrt(n))
    dist = width / k
    layout = {}

    i = 0
    for i in range(n):
        layout[self.nodes[i]] = ((i%k) *dist, -(np.floor(i/k))*dist)
        i += 1

    return layout

random

Position nodes uniformly at random in the unit square.

For every node, a position is generated by choosing each of dimension coordinates uniformly at random on the interval \([0.0, 1.0)\).

This algorithm can be enabled with the keywords: Random, random, rand, or None

Other Parameters:

Name Type Description
dimension int

Dimension of layout. Currently, only plots in 2 dimension are supported. Defaults to 2.

seed int

Set the random state for deterministic node layouts. If int, seed is the seed used by the random number generator, if None, the a random seed by created by the numpy random number generator is used.

Returns:

Name Type Description
layout dict

A dictionary of positions keyed by node

Source code in src/pathpyG/visualisations/layout.py
def random(self):
    """Position nodes uniformly at random in the unit square.

    For every node, a position is generated by choosing each of dimension
    coordinates uniformly at random on the interval $[0.0, 1.0)$.

    This algorithm can be enabled with the keywords: `Random`,
    `random`, `rand`, or `None`

    Keyword Args:
        dimension (int): Dimension of layout. Currently, only plots in 2 dimension are supported. Defaults to 2.
        seed (int): Set the random state for deterministic node layouts. If int, `seed` is
            the seed used by the random number generator, if None, the a random
            seed by created by the numpy random number generator is used.

    Returns:
        layout (dict): A dictionary of positions keyed by node
    """
    np.random.seed(self.seed)
    layout = np.random.rand(len(self.nodes), self.dimension)
    return dict(zip(self.nodes, layout))

rename_attributes staticmethod

Rename layout attributes.

In the style dictionary multiple keywords can be used to address attributes. These keywords will be converted to an unique key word, used in the remaining code.

keys other valid keys
fixed fixed_nodes, fixed_vertices, fixed_n, fixed_v
positions initial_positions, node_positions vertex_positions, n_positions, v_positions
Source code in src/pathpyG/visualisations/layout.py
@staticmethod
def rename_attributes(**kwds):
    """Rename layout attributes.

    In the style dictionary multiple keywords can be used to address
    attributes. These keywords will be converted to an unique key word,
    used in the remaining code.

    | keys | other valid keys |
    | ---- | ---------------- |
    | fixed | `fixed_nodes`, `fixed_vertices`, `fixed_n`, `fixed_v` |
    | positions | `initial_positions`, `node_positions` `vertex_positions`, `n_positions`, `v_positions` |
    """
    names = {'fixed': ['fixed_nodes', 'fixed_vertices',
                       'fixed_v', 'fixed_n'],
             'positions': ['initial_positions', 'node_positions',
                           'vertex_positions', 'n_positions',
                           'v_positions'],
             'layout_': ['layout_'],
             }

    _kwds = {}
    del_keys = []
    for key, value in kwds.items():
        for attr, name_list in names.items():
            for name in name_list:
                if name in key and name[0] == key[0]:
                    _kwds[key.replace(name, attr).replace(
                        'layout_', '')] = value
                    del_keys.append(key)
                    break
    # remove the replaced keys from the dict
    for key in del_keys:
        del kwds[key]

    return {**_kwds, **kwds}

layout

Function to generate a layout for the network.

This function generates a layout configuration for the nodes in the network. Thereby, different layouts and options can be chosen. The layout function is directly included in the plot function or can be separately called.

The layout function supports different network types and layout algorithm. Currently supported networks are:

  • cnet,
  • networkx,
  • igraph,
  • pathpyG
  • node/edge list

Currently supported algorithms are:

  • Fruchterman-Reingold force-directed algorithm
  • Uniformly at random node positions

The appearance of the layout can be modified by keyword arguments which will be explained in more detail below.

Parameters:

Name Type Description Default
network network object

Network to be drawn. The network can be a cnet, networkx, igraph, pathpy object, or a tuple of a node list and edge list.

required
**kwds Optional dict

Attributes used to modify the appearance of the layout. For details see below.

{}

Layout:

The layout can be modified by the following keyword arguments: Note: All layout arguments can be entered with or without layout_ at the beginning, e.g. layout_iterations is equal to iterations

Other Parameters:

Name Type Description
layout Optional dict or string

A dictionary with the node positions on a 2-dimensional plane. The key value of the dict represents the node id while the value represents a tuple of coordinates (e.g. \(n = (x,y)\)). The initial layout can be placed anywhere on the 2-dimensional plane.

Instead of a dictionary, the algorithm used for the layout can be defined via a string value. Currently, supported are:

  • Random layout, where the nodes are uniformly at random placed in the unit square.
  • Fruchterman-Reingold force-directed algorithm. In this algorithm, the nodes are represented by steel rings and the edges are springs between them. The attractive force is analogous to the spring force and the repulsive force is analogous to the electrical force. The basic idea is to minimize the energy of the system by moving the nodes and changing the forces between them.

The algorithm can be enabled with the keywords: | Algorithms | Keywords | | ---------- | -------- | | Random | Random, random, rand, None | |Fruchterman-Reingold | Fruchterman-Reingold, fruchterman_reingold, fr spring_layout, spring layout, FR |

force float

Optimal distance between nodes. If None the distance is set to 1/sqrt(n) where n is the number of nodes. Increase this value to move nodes farther apart.

positions dict

Initial positions for nodes as a dictionary with node as keys and values as a coordinate list or tuple. If None, then use random initial positions.

fixed list

Nodes to keep fixed at initial position.

iterations int

Maximum number of iterations taken. Defaults to 50.

threshold float

Threshold for relative error in node position changes. The iteration stops if the error is below this threshold. Defaults to 1e-4.

weight string

or None, optional (default = None) The edge attribute that holds the numerical value used for the edge weight. If None, then all edge weights are 1.

dimension int

Dimension of layout. Currently, only plots in 2 dimension are supported. Defaults to 2.

seed int

Set the random state for deterministic node layouts. If int, seed is the seed used by the random number generator, if None, the a random seed by created by the numpy random number generator is used.

In the layout style dictionary multiple keywords can be used to address attributes. These keywords will be converted to an unique key word, used in the remaining code.

keys other valid keys
fixed fixed_nodes, fixed_vertices, fixed_n, fixed_v
positions initial_positions, node_positions, vertex_positions, n_positions, v_positions

Examples:

For illustration purpose a similar network as in the python-igraph tutorial is used. Instead of igraph, the cnet module is used for creating the network.

Create an empty network object, and add some edges.

>>> net = Network(name = 'my tikz test network',directed=True)
>>> net.add_edges_from([('ab','a','b'), ('ac','a','c'), ('cd','c','d'),
...                     ('de','d','e'), ('ec','e','c'), ('cf','c','f'),
...                     ('fa','f','a'), ('fg','f','g'),('gg','g','g'),
...                     ('gd','g','d')])

Now a layout can be generated:

>>> layout(net)
{'b': array([0.88878309, 0.15685131]), 'd': array([0.4659341 , 0.79839535]),
'c': array([0.60386662, 0.40727962]), 'e': array([0.71073353, 0.65608203]),
'g': array([0.42663927, 0.47412449]), 'f': array([0.48759769, 0.86787594]),
'a': array([0.84154488, 0.1633732 ])}

Per default, the node positions are assigned uniform random. In order to create a layout, the layout methods of the packages can be used, or the position of the nodes can be directly assigned, in form of a dictionary, where the key is the node_id and the value is a tuple of the node position in \(x\) and \(y\).

Let us generate a force directed layout (e.g. Fruchterman-Reingold):

>>> layout(net, layout='fr')
{'g': array([-0.77646408,  1.71291126]), 'c': array([-0.18639655,0.96232326]),
'f': array([0.33394308, 0.93778681]), 'e': array([0.09740098, 1.28511973]),
'a': array([1.37933158, 0.23171857]), 'b': array([ 2.93561876,-0.46183461]),
'd': array([-0.29329793,  1.48971303])}

Note, instead of the command fr also the command Fruchterman-Reingold or any other command mentioned above can be used. For more information see table above.

In order to keep the properties of the layout for your network separate from the network itself, you can simply set up a Python dictionary containing the keyword arguments you would pass to layout and then use the double asterisk (**) operator to pass your specific layout attributes to layout:

>>> layout_style = {}
>>> layout_style['layout'] = 'Fruchterman-Reingold'
>>> layout_style['seed'] = 1
>>> layout_style['iterations'] = 100
>>> layout(net,**layout_style)
{'d': array([-0.31778276, 1.78246882]), 'f': array([-0.8603259, 0.82328291]),
'c': array([-0.4423771 , 1.21203895]), 'e': array([-0.79934355, 1.49000119]),
'g': array([0.43694799, 1.51428788]), 'a': array([-2.15517293, 0.23948823]),
'b': array([-3.84803812, -0.71628417])}
Source code in src/pathpyG/visualisations/layout.py
def layout(network, **kwds):
    """Function to generate a layout for the network.

    This function generates a layout configuration for the nodes in the
    network. Thereby, different layouts and options can be chosen. The layout
    function is directly included in the plot function or can be separately
    called.

    The layout function supports different network types and layout algorithm.
    Currently supported networks are:

    - `cnet`,
    - `networkx`,
    - `igraph`,
    - `pathpyG`
    - node/edge list

    Currently supported algorithms are:

    - Fruchterman-Reingold force-directed algorithm
    - Uniformly at random node positions

    The appearance of the layout can be modified by keyword arguments which will
    be explained in more detail below.

    Args:
        network (network object): Network to be drawn. The network can be a `cnet`, `networkx`, `igraph`, `pathpy` object, or a tuple of a node list and edge list.
        **kwds (Optional dict): Attributes used to modify the appearance of the layout. For details see below.

    # Layout:

    The layout can be modified by the following keyword arguments:
    Note: 
        All layout arguments can be entered with or without `layout_` at the beginning, e.g. `layout_iterations` is equal to `iterations`

    Keyword Args:
        layout (Optional dict or string): A dictionary with the node positions on a 2-dimensional plane. The
            key value of the dict represents the node id while the value
            represents a tuple of coordinates (e.g. $n = (x,y)$). The initial
            layout can be placed anywhere on the 2-dimensional plane.

            Instead of a dictionary, the algorithm used for the layout can be defined
            via a string value. Currently, supported are:

            - **Random layout**, where the nodes are uniformly at random placed in the
                unit square. 
            - **Fruchterman-Reingold force-directed algorithm**. In this algorithm, the
                nodes are represented by steel rings and the edges are springs between
                them. The attractive force is analogous to the spring force and the
                repulsive force is analogous to the electrical force. The basic idea is
                to minimize the energy of the system by moving the nodes and changing
                the forces between them. 

            The algorithm can be enabled with the keywords:
            | Algorithms | Keywords |
            | ---------- | -------- |
            | Random | `Random`, `random`, `rand`, `None` |
            |Fruchterman-Reingold | `Fruchterman-Reingold`, `fruchterman_reingold`, `fr spring_layout`, `spring layout`, `FR` |

        force (float): Optimal distance between nodes.  If None the distance is set to
            1/sqrt(n) where n is the number of nodes.  Increase this value to move
            nodes farther apart.
        positions (dict): Initial positions for nodes as a dictionary with node as keys and values
            as a coordinate list or tuple.  If None, then use random initial
            positions.
        fixed (list): Nodes to keep fixed at initial position.
        iterations (int): Maximum number of iterations taken. Defaults to 50.
        threshold (float): Threshold for relative error in node position changes.  The iteration
            stops if the error is below this threshold. Defaults to 1e-4.
        weight (string):  or None, optional (default = None)
            The edge attribute that holds the numerical value used for the edge
            weight. If None, then all edge weights are 1.
        dimension (int): Dimension of layout. Currently, only plots in 2 dimension are supported. Defaults to 2.
        seed (int): Set the random state for deterministic node layouts. If int, `seed` is
            the seed used by the random number generator, if None, the a random seed
            by created by the numpy random number generator is used.

    In the layout style dictionary multiple keywords can be used to address
    attributes. These keywords will be converted to an unique key word,
    used in the remaining code.

    | keys | other valid keys |
    | ---- | ---------------- |
    | fixed | `fixed_nodes`, `fixed_vertices`, `fixed_n`, `fixed_v` |
    | positions| `initial_positions`, `node_positions`, `vertex_positions`, `n_positions`, `v_positions` |

    Examples:
        For illustration purpose a similar network as in the `python-igraph` tutorial
        is used. Instead of `igraph`, the `cnet` module is used for creating the
        network.

        Create an empty network object, and add some edges.

        >>> net = Network(name = 'my tikz test network',directed=True)
        >>> net.add_edges_from([('ab','a','b'), ('ac','a','c'), ('cd','c','d'),
        ...                     ('de','d','e'), ('ec','e','c'), ('cf','c','f'),
        ...                     ('fa','f','a'), ('fg','f','g'),('gg','g','g'),
        ...                     ('gd','g','d')])

        Now a layout can be generated:

        >>> layout(net)
        {'b': array([0.88878309, 0.15685131]), 'd': array([0.4659341 , 0.79839535]),
        'c': array([0.60386662, 0.40727962]), 'e': array([0.71073353, 0.65608203]),
        'g': array([0.42663927, 0.47412449]), 'f': array([0.48759769, 0.86787594]),
        'a': array([0.84154488, 0.1633732 ])}

        Per default, the node positions are assigned uniform random. In order to
        create a layout, the layout methods of the packages can be used, or the
        position of the nodes can be directly assigned, in form of a dictionary,
        where the key is the `node_id` and the value is a tuple of the node position
        in $x$ and $y$.

        Let us generate a force directed layout (e.g. Fruchterman-Reingold):

        >>> layout(net, layout='fr')
        {'g': array([-0.77646408,  1.71291126]), 'c': array([-0.18639655,0.96232326]),
        'f': array([0.33394308, 0.93778681]), 'e': array([0.09740098, 1.28511973]),
        'a': array([1.37933158, 0.23171857]), 'b': array([ 2.93561876,-0.46183461]),
        'd': array([-0.29329793,  1.48971303])}

        Note, instead of the command `fr` also the command
        `Fruchterman-Reingold` or any other command mentioned above can be
        used. For more information see table above.

        In order to keep the properties of the layout for your network separate from
        the network itself, you can simply set up a Python dictionary containing the
        keyword arguments you would pass to [`layout`][pathpyG.visualisations.layout.layout] and then use the
        double asterisk (**) operator to pass your specific layout attributes to
        [`layout`][pathpyG.visualisations.layout.layout]:

        >>> layout_style = {}
        >>> layout_style['layout'] = 'Fruchterman-Reingold'
        >>> layout_style['seed'] = 1
        >>> layout_style['iterations'] = 100
        >>> layout(net,**layout_style)
        {'d': array([-0.31778276, 1.78246882]), 'f': array([-0.8603259, 0.82328291]),
        'c': array([-0.4423771 , 1.21203895]), 'e': array([-0.79934355, 1.49000119]),
        'g': array([0.43694799, 1.51428788]), 'a': array([-2.15517293, 0.23948823]),
        'b': array([-3.84803812, -0.71628417])}
    """
    # initialize variables
    _weight = kwds.get('weight', None)
    if _weight is None:
        _weight = kwds.get('layout_weight', None)

    # check type of network
    if 'cnet' in str(type(network)):
        # log.debug('The network is of type "cnet".')
        nodes = list(network.nodes)
        adjacency_matrix = network.adjacency_matrix(weight=_weight)

    elif 'networkx' in str(type(network)):
        # log.debug('The network is of type "networkx".')
        nodes = list(network.nodes())
        import networkx as nx
        adjacency_matrix = nx.adjacency_matrix(network, weight=_weight) # type: ignore
    elif 'igraph' in str(type(network)):
        # log.debug('The network is of type "igraph".')
        nodes = list(range(len(network.vs)))
        from scipy.sparse import coo_matrix
        A = np.array(network.get_adjacency(attribute=_weight).data)
        adjacency_matrix = coo_matrix(A)
    elif 'pathpyG' in str(type(network)):
        # log.debug('The network is of type "pathpy".')
        nodes = list(network.nodes)
        if _weight is not None:
            _w = True
        else:
            _w = False
        adjacency_matrix = network.get_sparse_adj_matrix()
    # elif isinstance(network, tuple):
    #     # log.debug('The network is of type "list".')
    #     nodes = network[0]
    #     from collections import OrderedDict
    #     edges = OrderedDict()
    #     for e in network[1]:
    #         edges[e] = e

    else:
        print('Type of the network could not be determined.'
                  ' Currently only "cnet", "networkx","igraph", "pathpy"'
                  ' and "node/edge list" is supported!')
        raise NotImplementedError

    # create layout class
    layout = Layout(nodes, adjacency_matrix, **kwds)
    # return the layout
    return layout.generate_layout()