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.