Strategies for Plotting Cycle Representatives

This tutorial contains tips that we found helpful when plotting cycle representatives.

Generate x-y-z coordinates for non-Euclidean data

Sometimes data doesn’t come with x-y-z coordinates. For example, it might be a graph or hypergraph. Check out the Vertex Embedding gallery for strategies to generate this data.

Multiple still frames (different angles, useful for papers and slide presentations)

If you’re trying to convey a complex 3D structure to an audience, you might not have the option to use a video. In this case, consider using multiple still frames from different angles.

  • See Styling in 3D for examples on generating multiple subplots in Plotly.

    • Or for a simpler approach, just rotate the camera manually and take screenshots.

Opaque colors

Opaque colors can make the 3d structure of an object more apparent.

  • This approach works well with multiple still frames. If you’re using multiple frames, then you don’t need to see “through” objects as much.

  • Try varying the colors of your triangles, too, to help distinguish different parts of the object.

Wire frame

Even when plotting 2-dimensional homology classes, sometimes it’s as good or better to plot a wire frame instead (meaning just the edges incident to the triangles):

  • saves computational resources (especially for large complexes)

  • can be easier to visualize

Try toggling the triangles on and off in the plots below to see the difference.

Example

Here’s an example to illustrate some of these strategies.

from networkx import triangles
import oat_python as oat
import plotly.graph_objects as go
import numpy as np

Generate a point cloud with the Stanford dragon and a sphere.

points                  =   oat.point_cloud.stanford_dragon()

Compute the persistent homology of the Vietoris-Rips complex.

# compute the minimum enclosing radius; all homology vanishes above this filtration parameter
enclosing               =   oat.dissimilarity.enclosing_radius_for_points(points)

# construct a distance matrix where values over enclosing + 0.0000000001 are removed
dissimilarity_matrix    =   oat.dissimilarity.sparse_matrix_for_points(
                                points                      =   points,
                                max_dissimilarity           =   enclosing + 0.0000000001, # adding 0.0000000001 avoids problems due to numerical error
                            )

# # format the input to the persistent homology solver
# dissimilarity_matrix    =   oat.dissimilarity.sparse_matrix_for_points(
#                                 points                          =   points,
#                                 max_dissimilarity               =   0.3,
#                             )

# build and factor the boundary matrix
decomposition           =  oat.core.vietoris_rips.BoundaryMatrixDecomposition(
                                dissimilarity_matrix            =   dissimilarity_matrix,
                                max_homology_dimension          =   1,
                                support_fast_column_lookup      =   True,
                            )

# extract the persistent homology dataframe, including cycle representatives and bounding chains
persistent_homology_dataframe            \
                        =   decomposition.persistent_homology_dataframe(
                                return_cycle_representatives    =   True,
                                return_bounding_chains          =   True,
                            )

Inspect the largest cycle representatives

persistent_homology_dataframe.nlargest(10, 'num_cycle_simplices')
dimension interval_length birth_filtration birth_simplex death_filtration death_simplex cycle_representative num_cycle_simplices bounding_chain num_bounding_simplices
id
1091 1 0.011571 0.009357 (377, 971) 0.020929 (614, 863, 971) simplex filtration coefficient 0 (6... 75 simplex filtration coefficient 0... 115.0
1203 1 0.011774 0.011787 (72, 325) 0.023561 (349, 495, 919) simplex filtration coefficient 0 (84... 75 simplex filtration coefficient 0... 123.0
1081 1 0.007711 0.009091 (713, 938) 0.016803 (501, 777, 886) simplex filtration coefficient 0 (45... 66 simplex filtration coefficient 0... 128.0
1090 1 0.009315 0.009327 (836, 970) 0.018642 (135, 944, 977) simplex filtration coefficient 0 (13... 59 simplex filtration coefficient 0... 109.0
1092 1 0.008609 0.009376 (368, 675) 0.017985 (326, 663, 893) simplex filtration coefficient 0 (6... 58 simplex filtration coefficient 0 ... 78.0
1111 1 0.007128 0.009686 (29, 568) 0.016813 (29, 45, 88) simplex filtration coefficient 0 (61... 47 simplex filtration coefficient 0 ... 61.0
1264 1 0.009288 0.014191 (670, 726) 0.023479 (670, 828, 943) simplex filtration coefficient 0 (3... 47 simplex filtration coefficient 0 ... 53.0
1158 1 0.014104 0.010779 (620, 742) 0.024883 (97, 380, 849) simplex filtration coefficient 0 ... 46 simplex filtration coefficient 0 ... 65.0
1088 1 0.008282 0.009269 (230, 925) 0.017551 (186, 216, 774) simplex filtration coefficient 0 (9... 34 simplex filtration coefficient 0 ... 32.0
1214 1 0.010349 0.012231 (264, 495) 0.022580 (264, 394, 718) simplex filtration coefficient 0 (9... 31 simplex filtration coefficient 0 ... 39.0


The largest cycle representative lies in row 1331 of the persistent homology dataframe. Extract the list of triangles in its bounding chain.

triangles           =   persistent_homology_dataframe["bounding_chain"][1203]["simplex"].tolist()

Plot the triangles

fig                 =   oat.plot.fig_3d_for_simplices(
                            simplices       =   triangles,
                            points          =   points,
                        )
fig.update_layout(template="plotly_dark", height=800)
fig


Color and shrink the vertices, to help differentiate portions of the object

fig.data[0].marker  =   dict(color = -points[:,2], colorscale="Peach", size=3)
fig


Make the triangles opaque, and color them by height.

fig                 =   oat.plot.fig_3d_for_simplices(
                            simplices         =   triangles,
                            points            =   points,
                            kwargs_points     =   dict(marker  =   dict( size=3, color=-points[:,2], colorscale="Peach")),
                            kwargs_triangles  =   dict(
                                                        intensity=[point[2] for point in points],
                                                        intensitymode="vertex",
                                                        colorscale="Peach",
                                                        opacity=1.0,
                                                        showscale=False,
                                                    )
                        )
fig.update_layout(template="plotly_dark", height=800)
fig


Generate multiple views from different angles

from plotly.subplots import make_subplots

# Create a new figure with 3 3D subplots
subfig = make_subplots(
    rows=3, cols=1,
    specs=[[{'type': 'scene'}], [{'type': 'scene'}], [{'type': 'scene'}]],
    subplot_titles=["View 1", "View 2", "View 3"],
    vertical_spacing=0.05 # Adjust this value to control spacing
)

# Add the same traces to each subplot
for trace in fig.data:
    subfig.add_trace(trace, row=1, col=1)
    subfig.add_trace(trace, row=2, col=1)
    subfig.add_trace(trace, row=3, col=1)

# Set different camera angles for each subplot
subfig.update_layout(
    scene=dict( camera=dict(eye=dict(x=0.0, y=1.0, z=1.0))),
    scene2=dict(camera=dict(eye=dict(x=0.9, y=1.2, z=1.2))),
    scene3=dict(camera=dict(eye=dict(x=0.7, y=-1.2, z=1.0))),
    height=700,
)

subfig.update_layout(
    # showlegend = False,
    height = 1800,
    width = None,
    template = "plotly_dark",
)

subfig


Total running time of the script: (0 minutes 3.285 seconds)

Gallery generated by Sphinx-Gallery