Nutils functions behave entirely like Numpy arrays, and can be manipulated as such, using a combination of operators, object methods, and methods found in the nutils.function module. Though powerful, the resulting code is often lengthy, littered with colons and brackets, and hard to read. Namespaces provide an alternative, cleaner syntax for a prominent subset of array manipulations.

A nutils.expression_v2.Namespace is a collection of nutils.function.Array functions. An empty nutils.expression_v2.Namespace is created as follows:

ns = Namespace()

New entries are added to a nutils.expression_v2.Namespace by assigning an nutils.function.Array to an attribute. For example, to assign the geometry geom to ns.x, simply type

ns.x = geom

You can now use ns.x where you would use geom. Usually you want to add the gradient, normal and jacobian of this geometry to the namespace as well. This can be done using nutils.expression_v2.Namespace.define_for naming the geometry (as present in the namespace) and names for the gradient, normal, and the jacobian as keyword arguments:

ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))

Note that any keyword argument is optional.

To assign a linear basis to ns.basis, type

ns.basis = topo.basis('spline', degree=1)

and to assign the discrete solution as the inner product of this basis with argument 'lhs', type

ns.u = function.dotarg('lhs', ns.basis)

You can also assign numbers and numpy.ndarray objects:

ns.a = 1
ns.b = 2
ns.c = numpy.array([1,2])
ns.A = numpy.array([[1,2],[3,4]])


In addition to inserting ready objects, a namespace's real power lies in its ability to be assigned string expressions. These expressions may reference any nutils.function.Array function present in the nutils.expression_v2.Namespace, and must explicitly name all array dimensions, with the object of both aiding readibility and facilitating high order tensor manipulations. A short explanation of the syntax follows; see nutils.expression_v2 for the complete documentation.

A term is written by joining variables with spaces, optionally preceeded by a single number, e.g. 2 a b. A fraction is written as two terms joined by /, e.g. 2 a / 3 b, which is equivalent to (2 a) / (3 b). An addition or subtraction is written as two terms joined by + or -, respectively, e.g. 1 + a b - 2 b. Exponentation is written by two variables or numbers joined by ^, e.g. a^2. Several trigonometric functions are available, e.g. 0.5 sin(a).

Assigning an expression to the namespace is then done as follows.

ns.e = '2 a / 3 b'
ns.e = (2*ns.a) / (3*ns.b) # equivalent w/o expression

The resulting ns.e is an ordinary nutils.function.Array. Note that the variables used in the expression should exist in the namespace, not just as a local variable:

localvar = 1
ns.f = '2 localvar'
# Traceback (most recent call last):
#   ...
# nutils.expression_v2.ExpressionSyntaxError: No such variable: `localvar`.
# 2 localvar
#   ^^^^^^^^

When using arrays in an expression all axes of the arrays should be labelled with an index, e.g. 2 c_i and c_i A_jk. Repeated indices are summed, e.g. A_ii is the trace of d and A_ij c_j is the matrix-vector product of d and c. You can also insert a number, e.g. c_0 is the first element of c. All terms in an expression should have the same set of indices after summation, e.g. it is an error to write c_i + 1.

When assigning an expression with remaining indices to the namespace, the indices should be listed explicitly at the left hand side:

ns.f_i = '2 c_i'
ns.f = 2*ns.c # equivalent w/o expression

The order of the indices matter: the resulting nutils.function.Array will have its axes ordered by the listed indices. The following three statements are equivalent:

ns.g_ijk = 'c_i A_jk'
ns.g_kji = 'c_k A_ji'
ns.g = ns.c[:,numpy.newaxis,numpy.newaxis]*ns.A[numpy.newaxis,:,:] # equivalent w/o expression

Function , introduced to the namespace with ~nutils.expression_v2.Namespace.define_for using geometry ns.x, returns the gradient of a variable with respect ns.x, e.g. the gradient of the basis is ∇_i(basis_n). This works with expressions as well, e.g. ∇_i(2 basis_n + basis_n^2) is the gradient of 2 basis_n + basis_n^2.

Manual evaluation

Sometimes it is useful to evaluate an expression to an nutils.function.Array without inserting the result in the namespace. This can be done using the <expression> @ <namespace> notation. An example with a scalar expression:

'2 a / 3 b' @ ns
# Array<>
(2*ns.a) / (3*ns.b) # equivalent w/o `... @ ns`
# Array<>

An example with a vector expression:

'2 c_i' @ ns
# Array<2>
2*ns.c # equivalent w/o `... @ ns`
# Array<2>

If an expression has more than one remaining index, the axes of the evaluated array are ordered alphabetically:

'c_i A_jk' @ ns
# Array<2,2,2>
ns.c[:,numpy.newaxis,numpy.newaxis]*ns.A[numpy.newaxis,:,:] # equivalent w/o `... @ ns`
# Array<2,2,2>