Plotting

This notebook can be downloaded here.

import pyrigi.frameworkDB as frameworks
import pyrigi.graphDB as graphs
from pyrigi import Graph, Framework

Methods Graph.plot() and Framework.plot() offer various plotting options. The default behaviour is the following:

G = Graph([(0,1), (1,2), (2,3), (0,3)])
G.plot()
../../_images/ec75340dedcd2218aa3afc4e5926f8f32918558cdb2c39555cf3a2bfd7cef1bb.png
F = Framework(G, {0: (0,0), 1: (1,1), 2: (3,1), 3: (2,0)})
F.plot()
../../_images/26f42b9eb3aff9003daec51dae5073f28c97b2bdb8c093e4815168691f673fd6.png

Graph layouts

By default, a placement is generated using spring_layout().

G = graphs.ThreePrism()
G.plot()
../../_images/80bc7b6efc9db383635d7cc689d2c3cea05a0879a0b218ce04e179bd21b8d2dc.png

Other options use random_layout(), circular_layout() or planar_layout():

G.plot(layout="random")
G.plot(layout="circular")
G.plot(layout="planar")
../../_images/dc6db39dc1b66c618f9f70225c4116443109e775a1dfd23df5205286c4a8c05e.png ../../_images/979e1013334aa07fca7b7b1824897443f57202d463843cf4efbced1e48ef8744.png ../../_images/b12818d5899e4aaea864e8a225cdf8344aa2bdbe098a882bfc4a67822fe30be2.png

One can also specify a placement of the vertices explicitly:

G.plot(placement={0: (0, 0), 1: (0, 2), 2: (2, 1), 3: (6, 0), 4: (6, 2), 5: (4, 1)})
../../_images/7f39d7865549971e62e3244a26715763b6cc72f2647f819c77ce4785b25c5c4c.png

Canvas options

The size of the canvas can be specified.

Square = frameworks.Square()
Square.plot(canvas_width=2)
../../_images/e9e8579e196d41e781bf01e2d23cb62b209583f74b1941228164a2900a818d79.png
Square.plot(canvas_height=2)
../../_images/6f18b8a2389200842b407e10e8878209a4a1145ea742b4ab16b7d69e9be40038.png
Square.plot(canvas_width=2, canvas_height=2)
../../_images/8eef52b7a92fdc4f651dbd33655207db6064715ad3b4e11827fb012274964db0.png

Also the aspect ratio:

Square.plot(aspect_ratio=0.4)
../../_images/4aee3a4c5605bdaf7a0dbe6687def8e853f9e6a52a235b1be7cc796e9ac25c05.png

Formatting

There are various options to format a plot. One can pass them as keywords arguments, for instance vertex color/size or label color/size can be changed.

G = Graph([[0,1]])
realization = {0: [0,0], 1: [1,0]}
G.plot(placement=realization, canvas_height=1, vertex_labels=False, vertex_color='green')
G.plot(placement=realization, canvas_height=1, vertex_size=1500, font_size=30, font_color='#FFFFFF')
../../_images/7f937a54b931d7c015e4dc1c57131df1beecd8f7bceeab48cb1e4043d9dd4ab6.png ../../_images/506f486a50157c71180193668eff32c8b8ac80eaaa8d7ec0f97c25646f8f1bb5.png

In order to format multiple plots using the same arguments, we can create an instance of class PlotStyle (see also PlotStyle2D) and pass it to each plot.

from pyrigi import PlotStyle
plot_style = PlotStyle(
    canvas_height=1,
    vertex_color='blue',
    vertex_labels=False,
)

There are various styles of vertices and edges.

G.plot(plot_style, placement=realization, vertex_shape='s', edge_style='-')
G.plot(plot_style, placement=realization, vertex_shape='o', edge_style='--')
G.plot(plot_style, placement=realization, vertex_shape='^', edge_style='-.')
G.plot(plot_style, placement=realization, vertex_shape='>', edge_style=':')
G.plot(plot_style, placement=realization, vertex_shape='v', edge_style='solid')
../../_images/2459434f11b6aefe72abf55397a46479ffe7a920ad4b33a7344b173fbfd8329f.png ../../_images/e44cb4fdc68cd403d5dc91398af0f15a39af608060789d48b361a629486f723e.png ../../_images/4c5a3fe20a592659d565db0ca069caf11b6c6e466457c36899f4e3708c7ad7c1.png ../../_images/0022e0e3daa02d785f40f96ebaafbee1be70a4aea0944eec120ba5b44e02106b.png ../../_images/5080f6ecdb7510e6e3bded12536e70d676ccfb752863a36db5f79ab643e9d4ec.png

We can also change some values of plot_style in two different ways. The first is using method PlotStyle.update().

plot_style.update(vertex_color='green')
G.plot(plot_style, placement=realization, vertex_shape='<', edge_style='dashed')
G.plot(plot_style, placement=realization, vertex_shape='d', edge_style='dashdot')
G.plot(plot_style, placement=realization, vertex_shape='p', edge_style='dotted')
../../_images/99012de9aa39547585fb9d5a7cb560a589dad16a888fdb012e06c598317012a1.png ../../_images/c8a950e0edeeb432464074ebc94344df59b8244bb34c48cf0cccfe4924d8854d.png ../../_images/9e958379b90de9d4c944b4ebabb90313f8c7ef2cfb49c1a8e2d427bf4747eb5e.png

The second is a direct assignment to an attribute.

plot_style.vertex_shape = 'h'
G.plot(plot_style, placement=realization, edge_width=3)
plot_style.vertex_shape = '8'
G.plot(plot_style, placement=realization, edge_width=5)
../../_images/e20f823218ce33af82ea9d9f08f97c1703225e80002aa2b26c24a95ab84d0c81.png ../../_images/cb67ae6e005477737b76ad12609431898cb88d24dd3ca75e38f39f03e83f70fc.png

Edge coloring

The color of all edges can be changed.

P = graphs.Path(6)
plot_style = PlotStyle(
    canvas_height=2,
    edge_width=5,
)
realization = {v: [v, 0] for v in P.vertex_list()}
P.plot(plot_style, placement=realization, edge_color='red')
../../_images/b0abf5e3b9697ea3140a0324e1573ab145c7d1f91f1bd988f0ab033356afcf50.png

If a partition of the edges is specified, then each part is colored differently.

P.plot(plot_style, placement=realization, edge_colors_custom=[[[0, 1], [2, 3]], [[1, 2]], [[5, 4], [4, 3]]])
../../_images/b8244f76c1a1c8c6b0902caf2e30d9108062ad5dfa5578c1c55e79e0cf2bba2b.png

If the partition is incomplete, the missing edges get PlotStyle.edge_color.

plot_style.update(edge_color='green')
P.plot(plot_style, placement=realization, edge_colors_custom=[[[0, 1], [2, 3]], [[5, 4], [4, 3]]])
../../_images/39b81e04c439575323bc82790c296efb295085bee9fd956817d9c0179a5734ad.png

Visually distinct colors are generated using the package distinctipy.

P30 = graphs.Path(30)
P30.plot( 
    vertex_size=15,
    vertex_labels=False,
    edge_colors_custom=[[e] for e in P30.edge_list()],
    edge_width=3
)
../../_images/48bb7482615fb709e2cceb8c0b4ba4fe2def993bc87e221dd8ad9e6371ef7a43.png

Another possibility is to provide a dictionary assigning to a color a list of edges. Missing edges again get PlotStyle.edge_color.

P.plot(plot_style,
       placement=realization,
        edge_colors_custom={
            "yellow": [[0, 1], [2, 3]],
            "#ABCDEF": [[5, 4], [4, 3]]
        },
)
../../_images/0e5cbf7f5f5a698abaa2923ffaf4c19104dbd4762bdbcee2fa6e6e3abbec37e5.png

Framework plotting

Currently, only plots of frameworks in the plane are implemented.

F = frameworks.Complete(9)
F.plot()
../../_images/627e495c21e3de69e0702839ce0ef958712b6385ee92acab4e359f362bf38072.png

The same formatting options as for graphs are available for frameworks. See also PlotStyle2D and PlotStyle3D.

F = frameworks.Complete(9)
F.plot(
    vertex_labels=False,
    vertex_color='#A2B4C6',
    edge_style='dashed',
    edge_width=2,
    edge_colors_custom={"pink": [[0,1],[3,6]], "lightgreen": [[2, 3], [3, 5]]}
)
../../_images/697c14f5d32dedd6379a4b4b78e9de5cb6093c70d3c37c0e7b206e4d88670811.png

Collinear Configurations

For collinear configurations and frameworks in \(\RR\), the edges are automatically visualized as arcs in \(\RR^2\)

F = Framework.Complete([[0],[1],[2]])
F.plot()
../../_images/11047c27cacce1e2b30d5650fc0a915d0d5f808577dab3f25299aaccda7a4330.png

Using the keyword arc_angles_dict, we are able to specify the pitch of the individual arcs. This parameter can be specified in radians as a float if the same pitch for every arc is desired and a list[float] or a dict[Edge, float] if the pitch is supposed to be provided for each arc individually.

F = Framework.Complete([[1],[3],[0],[2]])
F.plot(arc_angles_dict={(0,1):0.3, (0,2):0, (0,3):0, (1,2):0.5, (1,3):0, (2,3):-0.3})
../../_images/57e84cbf63fd8032161c0f7439ec926c86b50ecc02034bbc0a19cfc0d2f1345b.png

We can also enhance the visualization of other configurations using the boolean edges_as_arcs. This is particularly useful for visualizing almost or piecewise collinear configurations, but of course, it can also be applied to arbitrary frameworks. It is possible have fewer edges in the dict; the remaining edges are than padded with the default value arc_angle=math.pi/6. Here, we want to have some straight edges, so we redefine the arc_angle as \(0\).

F = frameworks.CnSymmetricFourRegular(n=8)
F.plot(edges_as_arcs=True, arc_angle=0, arc_angles_dict={(i,i+1):0.15 for i in range(7)} | {(0,7):-0.15})
../../_images/f2f35ad53a3a74bf484d98e224cbdbfdc9e5b37e85f53abcae570de45f609e4f.png

Infinitesimal Flexes

It is possible to include infinitesimal flexes in the plot. With the keyword inf_flex=n, we can pick the n-th nontrivial infinitesimal flex from a basis of the rigidity matrix’s kernel. There are several keywords that allow us to alter the style of the drawn arrows. A full list of the optional plotting parameters can be found in the API reference: PlotStyle.

G = Graph([[0, 1], [0, 2], [1, 2], [2, 3], [2, 4], [3, 4]])
realization = {0: [6, 8], 1: [6, -14], 2: [0, 0], 3: [-4, 4], 4: [-4, -4]}
F = Framework(G, realization)
F.plot(
    inf_flex=0,
    flex_width=4,
    flex_length=0.25,
    flex_color="darkgrey",
    flex_style="-",
    flex_arrow_size=15
)
../../_images/7aab3df1a4bd3deab45898ad89b3fa952218048c48f308c69ceb2086a227adf0.png

It is also possible to provide a specific infinitesimal flex with the following chain of commands:

F = frameworks.ThreePrism(realization="flexible")
inf_flex = F.nontrivial_inf_flexes()[0]
F.plot(inf_flex=inf_flex)
../../_images/902717a823c656d16a619a03c2b70b34454618877bf4503f1b9999e03a0e2ddb.png

It is important to use the same order of the vertices of F as Graph.vertex_list() when providing the infinitesimal flex as a Matrix. To circumvent that, we also support adding an infinitesimal flex as a Dict[Vertex, Sequence[Number]]. In both of the cases where the user provides an infinitesimal flex, it is internally checked whether the provided vector lies in the kernel of the rigidity matrix.

F = frameworks.Square()
inf_flex = {0: (1, -1), 1: (1, 1), 2: (-1, 1), 3: (-1, -1)}
F.plot(inf_flex=inf_flex)
../../_images/0fb4940fc2514ab274405ed49306b4c21aa669b5feb303c0de84002226258567.png

Equilibrium Stresses

We can also plot stresses. Contrary to flexes, stresses exist as edge labels. Analogous to the way that infinitesimal flexes can be visualized (see the previous section), a stress can be provided either as the n-th equilibrium stress, as a specific stress given by a Matrix or alternatively as a dict[Edge, Number]. It is internally checked, whether the provided stress lies in the cokernel of the rigidity matrix. We can specify the positions of the stress labels using the keyword stress_label_pos, which can either be set for all edges as the same float from \([0,1]\) or individually using a dict[DirectedEdge, float]. This float specifies the position on the line segment given by the edges. The missing edges labels are automatically centered on the edge. A full list of the optional plotting parameters can be found in the API reference: PlotStyle.

F = frameworks.Frustum(3)
F.plot(
    inf_flex=0,
    stress=0,
    stress_color = "orangered",
    stress_fontsize = 11,
    stress_label_positions = {(3,5): 0.6, (3,4):0.4, (4,5):0.4},
    stress_rotate_labels = False
)
../../_images/756bf13cd68cefd287e49577786eaae14772fe972b45409c0292e6a21103a0d0.png

The visualization of equilibrium stresses can be combined with the plotting of collinear configurations from a previous section that displays edges as curved arcs.

F = Framework.Complete([[1],[3],[0],[2]])
F.plot(stress=0, arc_angles_dict={(0,1):0.3, (0,2):0, (0,3):0, (1,2):0.5, (1,3):0, (2,3):-0.3})
../../_images/2c8734a7a76d723b8d03547f802f78c90cb6293ef74f765270bbe24d501f27a8.png

Plotting in 3 Dimensions

Plotting in 3 dimensions is also possible. The plot can be made interactive by using cell magic:

%matplotlib widget

Using the keyword axis_scales, we can decide whether we want to avoid stretching the framework ((1,1,1)) or whether we want to transform the framework to a different aspect ratio. The other possible keywords can be found in the corresponding API reference: plot3D().

F = frameworks.Complete(4, dim=3)
F.plot3D()

In addition, it is possible to animate a rotation sequence around a specified axis:

G = graphs.DoubleBanana()
F = Framework(G, realization={0:(0,0,-2), 1:(0,0,3), 2:(1.25,1,0.5), 3:(1.25,-1,0.5), 4:(3,0,0.5), 
                              5:(-1.25,-1,0.5), 6:(-1.2,1,0.5), 7:(-3,0,0.5)})
F.animate3D_rotation(rotation_axis=[0,0,1], axis_scales=(1,1,1))

We can return to the usual inline mode using the command %matplotlib inline. Note that triggering this command after using %matplotlib widget may cause the jupyter notebook to render additional pictures. If this behavior is undesirable, we suggest reevaluating the affected cells.

It is also possible to plot infinitesimal flexes and equilibrium stresses in 3D using the inf_flex and stress keywords, respectively. For details, the entire list of parameters concerning infinitesimal flexes and equilibrium stresses can be looked up in the corresponding API reference: PlotStyle.

F = frameworks.Octahedron(realization="Bricard_plane")
inf_flex = {v: [-qt for qt in q] 
            for v, q in F._transform_inf_flex_to_pointwise(F.inf_flexes()[0]).items()
}
F.plot(inf_flex=inf_flex, 
       stress=0,
       flex_length=0.25,
       stress_fontsize=11,
       axis_scales=(0.625,0.625,0.625),
       stress_label_positions={e: 0.6 for e in F.graph.edges}
)