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 Operator
s 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.