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 of nutils.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