Namespace
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]])
Expressions
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>