Solvers

Using topologies, bases and integrals, we now have the tools in place to start performing some actual functional-analytical operations. We start with what is perhaps the simplest of its kind, the least squares projection, demonstrating the different implementations now available to us and working our way up from there.

Taking the function as an example, to project it onto the discrete space means finding the discrete solution such that, for all discrete test functions :

In terms of the discrete coefficients this becomes the linear system . This is implemented as follows:

ns.u = topo.field('u', btype='spline', degree=1)
ns.v = topo.field('v', btype='spline', degree=1)
A, f = function.eval([
    topo.integral('v u dV' @ ns, degree=2).derivative('v').derivative('u'),
    topo.integral('v x_0^2 dV' @ ns, degree=2).derivative('v')])
u = numpy.linalg.solve(A, f)
# array([-0.01041667,  0.05208333,  0.23958333,  0.55208333,  0.98958333])

output

Alternatively, we can write this in the slightly more general form

res = topo.integral('v (u - x_0^2) dV' @ ns, degree=2)

After taking the derivative with respect to the test function 'v', the derivative to 'u' gives the above matrix , whereas substitution of the zero vector for 'u' yields -f. Nutils can compute those derivatives for you, using the method Array.derivative() to compute the derivative with respect to an nutils.function.Argument, returning a new nutils.function.Array.

A, f = function.eval([
    res.derivative('v').derivative('u'),
    -res.derivative('v')], arguments={'u': numpy.zeros(5)})
u = numpy.linalg.solve(A, f)
# array([-0.01041667,  0.05208333,  0.23958333,  0.55208333,  0.98958333])

The above three lines are so common that they are combined in the solve method of nutils.solver.System:

args = solver.System(res, trial='u', test='v').solve()
# {'u': array([-0.01041667,  0.05208333,  0.23958333,  0.55208333,  0.98958333])}

We can take this formulation one step further. Minimizing

for is equivalent to the above two variants, as the derivative of to equals .

sqr = topo.integral('(u - x_0)^2 dV' @ ns, degree=2)
solver.System([sqr.derivative('u')], trial='u').solve()
# {'u': array([-0.01041667,  0.05208333,  0.23958333,  0.55208333,  0.98958333])}

Since sqr.derivative('u') is vector valued, solve understands that its assignment is to make the entire vector zero, and no test function needs to be specified. However, we can go one step shorter by providing sqr as is, in which case solve interprets the absence of the test argument as being equal to the trial. The result is the same as before, with the added benefit of informing System that the linear system it is solving is symmetric.

solver.System(sqr, trial='u').solve()
# {'u': array([-0.01041667,  0.05208333,  0.23958333,  0.55208333,  0.98958333])}

The System object also supports solving a partial optimization problem via its solve_constraints method, so called because this is typically used for Dirichlet constraints. This method returns an array with nan ('not a number') for every entry for which the optimization problem is invariant, or to be precise, where the variation is below the mandatory droptol argument.

Putting it together

We are now in a position to consider again the Laplace problem stated above. The Dirichlet boundary condition at minimizes the following functional:

sqr = topo.boundary['left'].integral('(u - 1)^2 dS' @ ns, degree=2)

We use the solve_constraints method to solve this least squares problem for the single degree of freedom involved, leaving the others at not a number to signify that the are yet to be defined:

cons = solver.System(sqr, trial='u').solve_constraints(droptol=1e-15)
# {'u': array([ 1., nan, nan, nan, nan])}

The residual is implemented as

res = topo.integral('(∇_k(v) ∇_k(u) + 2 v) dV' @ ns, degree=2)
res -= topo.boundary['right'].integral('v dS' @ ns, degree=2)

We can use the system's solver method to solve this problem, with the constraints passed in via the constrain keyword argument:

args = solver.System(res, trial='u', test='v').solve(constrain=cons)
# {'u': array([1.    , 0.8125, 0.75  , 0.8125, 1.    ])}

Note, finally, that the solve method actually does not require the problem to be linear in 'u' at all; if it is not then it will automatically switch to Newton iterations unless otherwise specified.