Nutils 7 Hiyamugi
Nutils 7.0 was released on January 1st, 2022.
Nutils 7.1 was released on September 3rd, 2022.
Nutils 7.2 was released on November 4th, 2022.
Nutils 7.3 was released on June 20th, 2023.
What's New?
These are the main additions and changes since Nutils 6 Garak-Guksu.
Expression and Namespace Version 2
The nutils.expression
module has been renamed to nutils.expression_v1
, the
nutils.function.Namespace
class to nutils.expression_v1.Namespace
and the
nutils.expression_v2
module has been added, featuring a new
nutils.expression_v2.Namespace
. The version 2 of the namespace v2 has an
expression language that differs slightly from version 1, most notably in the
way derivatives are written. The old namespace remains available for the time
being. All examples are updated to the new namespace. You are encouraged to use
the new namespace for newly written code.
Changed: bifurcate has been replaced by spaces
In the past using functions on products of nutils.topology.Topology
instances
required using function.bifurcate
. This has been replaced by the concept of
'spaces'. Every topology is defined in a space, identified by a name (str
).
Functions defined on some topology are considered constant on other topologies
(defined on other spaces).
If you want to multiply two topologies, you have to make sure that the
topologies have different spaces, e.g. via the space
parameter of
nutils.mesh.rectilinear
. Example:
from nutils import mesh, function
Xtopo, x = mesh.rectilinear([4], space='X')
Ytopo, y = mesh.rectilinear([2], space='Y')
topo = Xtopo * Ytopo
geom = function.concatenate([x, y])
Changed: function.Array shape must be constant
Resulting from to the function/evaluable split introduced in #574, variable
length axes such as relating to integration points or sparsity can stay
confined to the evaluable layer. In order to benefit from this situation and
improve compatibility with Numpy's arrays, nutils.function.Array
objects are
henceforth limited to constant shapes. Additionally:
- The sparsity construct
nutils.function.inflate
has been removed; - The
nutils.function.Elemwise
function requires all element arrays to be of the same shape, and its remaining use has been deprecated in favor ofnutils.function.get
; - Aligning with Numpy's API,
nutils.function.concatenate
no longer automatically broadcasts its arguments, but instead demands that all dimensions except for the concatenation axis match exactly.
Changed: locate arguments
The nutils.topology.Topology.locate
method now allows tol
to be left
unspecified if eps
is specified instead, which is repurposed as stop
criterion for distances in element coordinates. Conversely, if only tol
is
specified, a corresponding minimal eps
value is set automatically to match
points near element edges. The ischeme
and scale
arguments are deprecated
and replaced by maxdist
, which can be left unspecified in general. The
optional weights
argument results in a sample that is suitable for
integration.
Moved: unit from types to separate module
The unit
type has been moved into its own nutils.unit
module, with the old
location types.unit
now holding a forward method. The forward emits a
deprecation warning prompting to change nutils.types.unit.create
(or its
shorthand nutils.types.unit
) to nutils.unit.create
.
Removed: loading libraries from .local
Libraries that are installed in odd locations will no longer be automatically
located by Nutils (see b8b7a6d5 for reasons). Instead the user will need to set
the appropriate environment variable, prior to starting Python. In Windows this
is the PATH
variable, in Linux and OS X LD_LIBRARY_PATH
.
Crucially, this affects the MKL libraries when they are user-installed via pip.
By default Nutils selects the best available matrix backend that it finds
available, which could result in it silently falling back on Scipy or Numpy. To
confirm that the path variable is set correctly run your application with
matrix=mkl
to force an error if MKL cannot be loaded.
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
Solve multiple residuals to multiple targets
In problems involving multiple fields, where formerly it was required to
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 deprecates target0
To explicitly refer to the history state in 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 nutils.solver.newton
, nutils.solver.minimize
or
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