Internal representation details

This chapter explains how UFL forms and expressions are represented in detail. Most operations are mirrored by a representation class, e.g., Sum and Product, which are subclasses of Expr. You can import all of them from the submodule ufl.classes by

from ufl.classes import *

Structure of a form

Each Form owns multiple Integral instances, each associated with a different Measure. An Integral owns a Measure and an Expr, which represents the integrand expression. The Expr is the base class of all expressions. It has two direct subclasses Terminal and Operator.

Subclasses of Terminal represent atomic quantities which terminate the expression tree, e.g. they have no subexpressions. Subclasses of Operator represent operations on one or more other expressions, which may usually be Expr subclasses of arbitrary type. Different Operators may have restrictions on some properties of their arguments.

All the types mentioned here are conceptually immutable, i.e. they should never be modified over the course of their entire lifetime. When a modified expression, measure, integral, or form is needed, a new instance must be created, possibly sharing some data with the old one. Since the shared data is also immutable, sharing can cause no problems.

General properties of expressions

Any UFL expression has certain properties, defined by functions that every Expr subclass must implement. In the following, u represents an arbitrary UFL expression, i.e. an instance of an arbitrary Expr subclass.

operands

u.operands() returns a tuple with all the operands of u, which should all be Expr instances.

reconstruct

u.reconstruct(operands) returns a new Expr instance representing the same operation as u but with other operands. Terminal objects may simply return self since all Expr instance are immutable. An important invariant is that u.reconstruct(u.operands()) == u.

cell

u.cell() returns the first Cell instance found in u. It is currently assumed in UFL that no two different cells are used in a single form. Not all expression define a cell, in which case this returns None and u is spatially constant. Note that this property is used in some algorithms.

shape

u.shape() returns a tuple of integers, which is the tensor shape of u.

free_indices

u.free_indices() returns a tuple of Index objects, which are the unassigned, free indices of u.

index_dimensions

u.index_dimensions() returns a dict mapping from each Index instance in u.free_indices() to the integer dimension of the value space each index can range over.

str(u)

str(u) returns a human-readable string representation of u.

repr(u)

repr(u) returns a Python string representation of u, such that eval(repr(u)) == u holds in Python.

hash(u)

hash(u) returns a hash code for u, which is used extensively (indirectly) in algorithms whenever u is placed in a Python dict or set.

u == v

u == v returns true if and only if u and v represents the same expression in the exact same way. This is used extensively (indirectly) in algorithms whenever u is placed in a Python dict or set.

About other relational operators

In general, UFL expressions are not possible to fully evaluate since the cell and the values of form arguments are not available. Implementing relational operators for immediate evaluation is therefore impossible.

Overloading relational operators as a part of the form language is not possible either, since it interferes with the correct use of container types in Python like dict or set.

Elements

All finite element classes have a common base class FiniteElementBase. The class hierarchy looks like this:

TODO: Class figure. .. TODO: Describe all FiniteElementBase subclasses here.

Terminals

All Terminal subclasses have some non-Expr data attached to them. ScalarValue has a Python scalar, Coefficient has a FiniteElement, etc.

Therefore, a unified implementation of reconstruct is not possible, but since all Expr instances are immutable, reconstruct for terminals can simply return self. This feature and the immutability property is used extensively in algorithms.

Operators

All instances of Operator subclasses are fully specified by their type plus the tuple of Expr instances that are the operands. Their constructors should take these operands as the positional arguments, and only that. This way, a unified implementation of reconstruct is possible, by simply calling the constructor with new operands. This feature is used extensively in algorithms.

Extending UFL

Adding new types to the UFL class hierarchy must be done with care. If you can get away with implementing a new operator as a combination of existing ones, that is the easiest route. The reason is that only some of the properties of an operator is represented by the Expr subclass. Other properties are part of the various algorithms in UFL. One example is derivatives, which are defined in the differentiation algorithm, and how to render a type to the dot formats. These properties could be merged into the class hierarchy, but other properties like how to map a UFL type to some ffc or dolfin type cannot be part of UFL. So before adding a new class, consider that doing so may require changes in multiple algorithms and even other projects.