The purpose of this notebook is to provide a tutorial for the DMPlex features in PETSc. DMPlex and its sub-objects are an attempt to properly abstract out the concept of grids and the assignment of degree of freedom information to entities in that grid. The hope is that this will allow for easy implementation of different discretization ideas and subsequently lead to their fair comparison. To be able to use DMPlex you need to speak its language and understand how to get the information your methods need. In this tutorial I will explain and demonstrate the functionality of the DMPlex API.
In [1]:
import sys,petsc4py
petsc4py.init(sys.argv)
from petsc4py import PETSc
import numpy as np
One way to create a DMPlex is to specify coordinates of vertices and cell connectivities. Here we encode a simple 2 by 2 element mesh of quads.
In [2]:
dim = 2
coords = np.asarray([[0.0, 0.0],
[0.5, 0.0],
[1.0, 0.0],
[0.0, 0.5],
[0.5, 0.5],
[1.0, 0.5],
[0.0, 1.0],
[0.5, 1.0],
[1.0, 1.0]])
cells = np.asarray([[0,1,4,3],
[1,2,5,4],
[3,4,7,6],
[4,5,8,7]],dtype='int32')
Now we initialize the DMPlex using this mesh information.
In [3]:
plex = PETSc.DMPlex().createFromCellList(dim,cells,coords)
PETSc converts the mesh into their abstraction of a topology which they store as a Hasse Diagram. Essentially this is a list of integers which encodes all the entities of each dimenion. We can use the view method to see what the DMPlex has encoded (broken, prints to the terminal but not here, capture magic doesn't work).
In [4]:
plex.view()
The view reveals that we have a mesh in 2 dimensions and that we have 9 0-cells (vertices), 12 1-cells (edges), and 4 2-cells (quads). All mesh entities are stored as integers in a single array called a chart. Each entity in the chart is called a point. (At this point it would be good to make some kind of plot with all points numbered, I suggest sketching one.)
The cone of a point consists of the points of a dimension lower which make up that entity. So we can loop through the chart and print out each points' cone.
In [17]:
pStart,pEnd = plex.getChart()
for i in range(pStart,pEnd):
print "point =", i, "\tcone =", plex.getCone(i)
Note that the numbering is completely different from our original mesh encoding. Here we summarize what we observe from the cones of the chart entities.
Similarly, each point has a support. The support of a point is the list which consists of points of a higher dimension which contain the point in its cone. So we can now repeat the above exercise but for the support.
In [18]:
for i in range(pStart,pEnd):
print "point =", i, "\tsupport =", plex.getSupport(i)
So the DMPlex is a dimension-independent, low memory, abstract representation of a topology which we use to represent grid objects. The DMPlex knows nothing about elements, basis functions, fluxes, degrees of freedom, etc. It is just the topology itself and is completely general. DMPlex can be used to construct any kind of topological relation. Here we created one from a cell list and then accessed its cone/support information. A DMPlex can also be built by hand using the appropriate set routines or with other kinds of constructors available in the API.
DMPlex provides support for the labeling of points. This can be helpful if you would like to flag certain entities for some reason. By default, the DMPlex comes with a label called 'depth'. This labels each entity based on how deep it is in the chart. You could also think of it as the dimensionality of the objects. Here we can check that the label does exist.
In [22]:
for i in range(plex.getNumLabels()):
name = plex.getLabelName(i)
print "label name = %s" % name, "\tlabel size = %d" % plex.getLabelSize(name)
So the label 'depth' does exist and we see that there are 3 different entries. Now we will loop over each item in the DMPlex and print the value of the label.
In [20]:
for i in range(pStart,pEnd):
print "point =",i, "\tlabel(depth) = %d" % plex.getLabelValue("depth",i)
The depths listed match our intuition of which entities were which when we looked at the cones and support. The DMPlex has support for identifying the range of indices in the chart which correspond to each value of the depth, the so-called depth stratum. (I do not understand what height stratum is for yet)
In [9]:
for i in range(plex.getDepth()+1):
print "depth = %d" % i,"\tdepth stratum = ",plex.getDepthStratum(i),"\theight stratum = ",plex.getHeightStratum(i)
We can also use labels to mark, say, boundary edges. These are the edges with only 1 entry in the support.
In [23]:
plex.createLabel("boundary")
for i in range(pStart,pEnd):
if plex.getLabelValue("depth",i) == 1: # this is an edge
if plex.getSupportSize(i) == 1: # only one cell has it as an edge
plex.setLabelValue("boundary",i,1)
print "point =", i, "\tlabel(boundary) = %d" % plex.getLabelValue("boundary",i)
The default values are set to -1 and the boundary edges were marked with a 1. Labels aren't so useful on their own--the useful part about labeling things is that you can also get index sets of all entities with the same value of label. This means that if we wanted an index set which maps vertex numbers to the chartID we can get the PETSc IS for the 'depth' label for a value of 0 (again view doesn't print here).
In [11]:
vis = plex.getStratumIS("depth",0)
vis.view()
Note that there are also routines which would appear to do the same as the above. However, their index sets return a local to global mapping of vertices (smallest depth in chart) and cells (largest depth in chart) useful in parallel.
In [12]:
vis = plex.getVertexNumbering()
vis.view()
cis = plex.getCellNumbering()
cis.view()
Many times in performing grid operations, you need to know how lower and/or higher dimensional items are connected to each other. In the PETSc parlance, these are called meets and joins. A meet of a set of points is the intersection of the points' cones and a join is the intersection of the points' support. Here we demonstrate the concept with a few examples.
In [13]:
# Two cells, meet is the common edge, no join
pnts = [0,1]
print "meet =",plex.getMeet(pnts),"\tjoin =",plex.getJoin(pnts)
In [14]:
# Two edges, meet is the common vertex, join is the cell to which they are both connected
pnts = [14,19]
print "meet =",plex.getMeet(pnts),"\tjoin =",plex.getJoin(pnts)
In [15]:
# Two vertices, no meet, join is the common edge to which they are both connected
pnts = [8,11]
print "meet =",plex.getMeet(pnts),"\tjoin =",plex.getJoin(pnts)
The transitive closure of a point in the DMPlex is a list of all reachable points from the given point, by default in the 'in-edge' direction. The transitive closure is then a set created by recursively taking the union on all points in the cone and its cones. In other words, it is all points of lower or equal dimension that this point can "reach". The routine also returns an array parallel to the closure array which defines how the points are oriented relative to the give point (e.g. for a cell, you might need to flip some edges to follow a right-handed convention). There is a flag in the routine, useCone, which if set to False will perform the same operation but in the 'out-edge' direction (that is, instead of recursively operating on cones, it will use the supports).
In [16]:
for i in range(pStart,pEnd):
coneclose,orient = plex.getTransitiveClosure(i)
suppclose,orient = plex.getTransitiveClosure(i,useCone=False)
print "point =",i,"\tclosure(cone) =",coneclose,"\tclosure(supp) =",suppclose
In [16]: