This walkthrough will take you through all the steps involved in adding an implementation to DefElement.
Detailed documentation of the class methods and variables used in this walkthrough, as well as other optional method, can be found in the file defelement/implementations/core.py.
As an example, this walkthrough will look at adding the simplefem library to DefElement. simplefem has been created to serve as an illustration library for this walkthrough, and should not be considered as a fully fledged library. It only features Lagrange elements on triangles and can evaluate their basis functions. For example, the following snippet will create a Lagrange element on a triangle with 10 basis functions and evaluate its basis function with index 5 at the point (0.3, 0.1):
import simplefem
import numpy as np
e = simplefem.lagrange_element(10)
value = e.evaluate(5, np.array([0.3, 0.1]))
To add simplefem to DefElement, a file in the folder defelement/implementations must be created. In this case, we called the file simplefem.py. In this file, we begin by importing functionality from the file core.py:
import typing
from numpy import float64
from numpy.typing import NDArray
from defelement.element import Element
from defelement.implementations.core import Implementation
class SimplefemImplementation(Implementation):
"""Simplefem implementation."""
In order to add information about a finite element library to DefElement, we must implement four class methods and set the values of four variables in this class.
The first method to implement is format. The formatting string and parameters set in DefElement's .def files will be passed into this method. For simplefem, this method simply returns the format string included in the .def file:
@classmethod
def format(cls, string: str, params: dict[str, typing.Any]) -> str:
"""Format implementation string."""
return string
In the file elements/lagrange.def the following lines are included in the implementations section:
implementations:
...
simplefem:
equispaced:
triangle: lagrange_element DEGREEMAP=(k+1)*(k+2)//2
Therefore, the format class method will be called for an equispaced Lagrange element on a triangle, with string="lagrange_element" and params={} (note that DEGREEMAP uses the syntax for a parameter but is not included in param due to being a special parameter: we will return to DEGREEMAP in the next section).
The value returned by format will be displayed on the element's page. In the case, the class method returns the string lagrange_element (which is the function in simplefem used to create the element).
To generate example code that will be displayed on an element's page, DefElement will use the class methods example_import and single_example. The class method example_import returns the import statements to include at the start of the example code. The class method single_example returns Python code that will create the element: the inputs to this method are the string included in the .def file (name); the name of the reference cell for this example (reference); the degree for this example (degree); and the parameters included in the .def file (params). The additional inputs element and example are the DefElement Element object and the raw example information: these may be needed in some more complex cases.
For simplefem, these class methods are defined as follows:
@classmethod
def example_import(cls) -> str:
"""Get imports to include at start of example."""
return "import simplefem"
@classmethod
def single_example(
cls,
name: str,
reference: str,
degree: int,
params: dict[str, str],
element: Element,
example: str,
) -> str:
"""Generate code for a single example."""
return f"element = simplefem.{name}({degree})"
The funtion example_import gives code to import simplefem. The inputs to class method single_example will be name="lagrange_element", reference="triangle", and params={}. The degree used in DefElement (in this case, the polynomial subdegree 1, 2, or 3) will be substituted into the DEGREEMAP special parameter as k before this method is called (so the values 3, 6, and 10 will be passed in). In this way, DefElement's notion of degree can be automatically converted to the number of points input that simplefem uses.
The final class method that must be implemented is version, which should return the version number of the implementation library. For simplefem, this is implemented as follows:
@classmethod
def version(cls) -> str:
"""Get the version number of this implementation."""
import simplefem
return simplefem.__version__
When an implementation can be installed from PyPI, the decorator pypi_name can be used. This decorator will automatically implement the method version and set the variable install (as described in the next section). For the ndelement library, for example, pypi_name is used as follows:
from defelement.implementations.core import pypi_name
@pypi_name("ndelement")
class NDElementImplementation(Implementation):
"""NDElement implementation."""
Finally, four variables need to be defined:
For simplefem, these variables are set to the following values. In general, it is preferable for the install to be done from PyPI rather than via git, but as simplefem is merely an example library and has no official release, installing via git is used in this case.
id = "simplefem"
name = "simplefem"
url = "https://github.com/DefElement/simplefem"
install = "pip3 install git+https://github.com/DefElement/simplefem"
We have now added everything that we need for an implementation to be included on DefElement. As simplefem is an example library, it is by default hidden from the Lagrange element page. If it were included, its section on the page would look like this:
| simplefem | lagrange_element↓ Show simplefem examples ↓ This implementation is correct for all the examples below that it supports. Note: This implementation uses an alternative value of degree for this element |
As well as giving information about implementations and code snippets for creating elements, DefElement is able to verify that the basis functions of elements provided by different implementations span the same polynomial spaces. If you want your implementation to be verified, you will need to additionally implement the class method verify and set the variable verification to True.
The method verify takes the string included in the .def file (name); the name of the reference cell for this example (reference); the degree for this example (degree); and the parameters included in the .def file (params). The additional inputs element and example are the DefElement Element object and the raw example in case these are needed. The method should return a list of DOFs associated with each entity of each dimension (as detailed below) and a function that maps a set of points on the DefElement reference cell to a table of values: this table will be a three-dimensional Numpy array with the value table[i][j][k] giving the jth component of the kth basis function evalutated at the ith point.
We now look in detail at the implementation of this function for simplefem. We begin with the def statement defining the method:
@classmethod
def verify(
cls,
name: str,
reference: str,
degree: int,
params: dict[str, str],
element: Element,
example: str,
) -> tuple[list[list[list[int]]], typing.Callable[[NDArray[float64]], NDArray[float64]]]:
"""Get verification data."""
The method begins by importing simplefem and numpy: it is important that these are imported inside the method so that import errors can be caught when running verification.
import simplefem
import numpy as np
The method then uses getattr to get the relevant element creation function and creates the element (e). As only one element is implemented in simplefem, name will always be equal to "lagrange_element", but this function has been written with a more general library in mind.
e = getattr(simplefem, name)(degree)
We next make lists of which degrees of freedom (DOFs) are associated with each sub-entity: the variable entity_dofs that is created is a list of lists of lists where entity_dofs[i][j] is a list of DOFs associated with the jth subentity of dimension i. This variable will be one of the two value returned by this function. The numbering of DOFs in simplefem is not the same as in DefElement's examples, so we do this by looping over the points used to define each basis function and checking if they're at a vertex or on an edge. Luckily, the ordering of points on each edge and inside the triangle matches the ordering used by DefElement, so these lists do not need to be permuted once created.
entity_dofs: list[list[list[int]]] = [[[], [], []], [[], [], []], [[]]]
for i, p in enumerate(e.evaluation_points):
# DOFs associated with vertices
if np.allclose(p, [-1, 0]):
entity_dofs[0][0].append(i)
elif np.allclose(p, [1, 0]):
entity_dofs[0][1].append(i)
elif np.allclose(p, [0, 1]):
entity_dofs[0][2].append(i)
# DOFs associated with edges
elif np.isclose(p[1], 0):
entity_dofs[1][0].append(i)
elif np.isclose(p[1] - p[0], 1):
entity_dofs[1][1].append(i)
elif np.isclose(p[1] + p[0], 1):
entity_dofs[1][2].append(i)
# DOFs associated with interior of cell
else:
entity_dofs[2][0].append(i)
Next, we define a function tabulate that takes a set of points as an inputs and returns the values of all of the basis functions of the element at every point. The points input to this function are points on the reference cell as used by DefElement and so points and function values may need to be mapped if the implementation uses an alternative reference cell.
DefElement's reference cell has vertices at (0,0), (1,0) and (0,1), while simplefem's reference cell has vertices at (-1,0), (1,0) and (1,0), so we define mapped_points to be the points in simplefem's reference cell that correspond to the points that are input.
We then loop through each point and each basis function and use e.evaluate to evaluate the value of the basis function at the point.
def tabulate(points):
mapped_points = np.array([[2 * p[0] + p[1] - 1, p[1]] for p in points])
table = np.zeros([points.shape[0], 1, degree])
for i, p in enumerate(mapped_points):
for j in range(degree):
table[i, 0, j] = e.evaluate(j, p)
return table
Finally, we return entity_dofs and the function tabulate and set the class variable verification to True:
return entity_dofs, tabulate
verification = True
As it is an example library, simplefem is hidden from the main verification index pages, but can be viewed at defelement.org/verification/simplefem.html.