----------------------------
- Function module split into ``function`` and ``evaluable``
The function module has been split into a high-level, numpy-like ``function``
module and a lower-level ``evaluable`` module. The ``evaluable`` module is
agnostic to the so-called points axis. Scripts that don't use custom
implementations of ``function.Array`` should work without modification.
Custom implementations of the old ``function.Array`` should now derive from
``evaluable.Array``. Furthermore, an accompanying implementation of
``function.Array`` should be added with a ``prepare_eval`` method that
returns the former.
The following example implementation of an addition
>>> class Add(function.Array):
... def __init__(self, a, b):
... super().__init__(args=[a, b], shape=a.shape, dtype=a.dtype)
... def evalf(self, a, b):
... return a+b
should be converted to
>>> class Add(function.Array):
... def __init__(self, a: function.Array, b: function.Array) -> None:
... self.a = a
... self.b = b
... super().__init__(shape=a.shape, dtype=a.dtype)
... def prepare_eval(self, **kwargs) -> evaluable.Array:
... a = self.a.prepare_eval(**kwargs)
... b = self.b.prepare_eval(**kwargs)
... return Add_evaluable(a, b)
...
>>> class Add_evaluable(evaluable.Array):
... def __init__(self, a, b):
... super().__init__(args=[a, b], shape=a.shape, dtype=a.dtype)
... def evalf(self, a, b):
... return a+b
- Functions generating or consuming axes in expressions
The expression syntax now supports functions that generate and/or consume
axes. The namespace has built-in support for ``sum``, ``norm2`` and ``J``
(jacobian)::
'sum:i(u_ij)' sum the first axis of `u`
'norm2:i(u_i)' 2-norm of `u`
'J:i(x_i)' jacobian of `x`
If all axes of function arguments are consumed, it is allowed to omit the
indices::
'norm2(u)'
'J(x)'
- New derivative and normal syntax
The :class:`~nutils.function.Namespace` now supports writing derivatives and
normals as functions::
'd(u, x_i)' alternative for 'u_,i', deprecates 'u_,x_i'
'd(u, x_i, x_j)' alternative for 'u_,ij'
'surfgrad(u, x_i)' alternative for 'u_;i', deprecates 'u_;x_i'
'd(u, ?a)' deprecates 'u_,?a'
'n(x_i)' deprecates 'n:x_i'
- User-defined functions in :class:`~nutils.function.Namespace`
The :class:`~nutils.function.Namespace` can be initialized with a dictionary
of user-defined functions::
>>> def mul(a, b):
... return a[(...,)+(None,)*b.ndim] * b[(None,)*a.ndim]
>>> ns = Namespace(functions=dict(mul=mul))
>>> 'mul(a_i, b_j)' ns equivalent to `'a_i b_j' ns`
- Solve multiple residuals to multiple targets
In problems involving multiple fields, where formerly it was required to
:func:`nutils.function.chain` the bases in order to construct and solve a
block system, an alternative possibility is now to keep the residuals and
targets separate and reference the several parts at the solving phase::
old, still valid approach
>>> ns.ubasis, ns.pbasis = function.chain([ubasis, pbasis])
>>> ns.u_i = 'ubasis_ni ?dofs_n'
>>> ns.p = 'pbasis_n ?dofs_n'
new, alternative approach
>>> ns.ubasis = ubasis
>>> ns.pbasis = pbasis
>>> ns.u_i = 'ubasis_ni ?u_n'
>>> ns.p = 'pbasis_n ?p_n'
common: problem definition
>>> ns.σ_ij = '(u_i,j + u_j,i) / Re - p δ_ij'
>>> ures = topo.integral('ubasis_ni,j σ_ij d:x d:x' ns, degree=4)
>>> pres = topo.integral('pbasis_n u_,kk d:x' ns, degree=4)
old approach: solving a single residual to a single target
>>> dofs = solver.newton('dofs', ures + pres).solve(1e-10)
new approach: solving multiple residuals to multiple targets
>>> state = solver.newton(['u', 'p'], [ures, pres]).solve(1e-10)
In the new, multi-target approach, the return value is no longer an array but
a dictionary that maps a target to its solution. If additional arguments were
specified to newton (or any of the other solvers) then these are copied into
the return dictionary so as to form a complete state, which can directly be
used as an arguments to subsequent evaluations.
If an argument is specified for a solve target then its value is used as an
initial guess (newton, minimize) or initial condition (thetamethod). This
replaces the ``lhs0`` argument which is not supported for multiple targets.
- New thetamethod argument ``historysuffix`` deprecates ``target0``
To explicitly refer to the history state in :func:`nutils.solver.thetamethod`
and its derivatives ``impliciteuler`` and ``cranknicolson``, instead of
specifiying the target through the ``target0`` parameter, the new argument
``historysuffix`` specifies only the suffix to be added to the main target.
Hence, the following three invocations are equivalent::
deprecated
>>> solver.impliciteuler('target', residual, inertia, target0='target0')
new syntax
>>> solver.impliciteuler('target', residual, inertia, historysuffix='0')
equal, since '0' is the default suffix
>>> solver.impliciteuler('target', residual, inertia)
- In-place modification of newton, minimize, pseudotime iterates
When :class:`nutils.solver.newton`, :class:`nutils.solver.minimize` or
:class:`nutils.solver.pseudotime` are used as iterators, the generated
vectors are now modified in place. Therefore, if iterates are stored for
analysis, be sure to use the ``.copy`` method.
- Deprecated ``function.elemwise``
The function ``function.elemwise`` has been deprecated. Use
``function.Elemwise`` instead::
>>> function.elemwise(topo.transforms, values) deprecated
>>> function.Elemwise(values, topo.f_index) new
- Removed ``transforms`` attribute of bases
The ``transforms`` attribute of bases has been removed due to internal
restructurings. The ``transforms`` attribute of the topology on which the
basis was created can be used as a replacement::
>>> reftopo = topo.refined
>>> refbasis = reftopo.basis(...)
>>> supp = refbasis.get_support(...)
>>> topo = topo.refined_by(refbasis.transforms[supp]) no longer valid
>>> topo = topo.refined_by(reftopo.transforms[supp]) still valid