How to check if the NonlinearProblem is setup properly?

Hello,

I am new to FEniCS and working towards solving nonlinear systems with the first-order generalized-alpha method (paper here). To get started, I’ve modified the dolfinx heat conduction tutorial (found here to solve with fem.petsc.NonlinearProblem() with generalized alpha. The solution looks reasonable at first glance, however, it does not produce the expected error convergence in the L^2-Norm.

Because the difference here is changing from linear to non-linear, I suspect there is a problem with how the NonlinearProblem is set up or how the nonlinear problem interprets the weak form. Both of these concern me because of the form that the Residual and Jacobian take for the generalized alpha method in terms of intermediate values of the solution variables. The code is below - thanks in advance!

import numpy
from dolfinx import mesh, fem, nls, plot
import ufl
from mpi4py import MPI
from petsc4py import PETSc

def run_problem(dt, rhoinf):
    t = 0 # Start time
    T = 2 # End time
    num_steps = round(T/dt) # Number of time steps
#     dt = (T-t)/num_steps # Time step size
    alpha = 3
    beta = 1.2

    # generalized-alpha mehtod parameters
    #--------------------------------------------------------------------------------    
    # rhoinf = 0.5 # rho infinity for the method, 1 corresponds to midpoint method
    alpha_m = (1/2)*( (3-rhoinf)/(1+rhoinf))
    alpha_f = 1/(1+rhoinf)
    gamma = (1/2) + alpha_m - alpha_f

    # domain setup
    #--------------------------------------------------------------------------------  
    nx, ny = int(1/dt), int(1/dt)
    domain = mesh.create_unit_square(MPI.COMM_WORLD, nx, ny, mesh.CellType.triangle)
    V = fem.FunctionSpace(domain, ("CG", 1))

    class exact_solution():
        def __init__(self, alpha, beta, t):
            self.alpha = alpha
            self.beta = beta
            self.t = t
        def __call__(self, x):
            return 1 + x[0]**2 + self.alpha * x[1]**2 + self.beta * self.t

    class exact_du():
        def __init__(self, alpha, beta, t):
            self.alpha = alpha
            self.beta = beta
            self.t = t
        def __call__(self, x):
            return 0*(1 + x[0]**2 + self.alpha * x[1]**2) + self.beta * self.t

    # Generalized-alpha helper functions    
    #--------------------------------------------------------------------------------    
    def predict_u(u_old):
        return u_old

    def predict_du(du_old, g):
        return (g-1)/g * du_old

    def correct_du(u_naf,u_old,du_old,am,af,g,dt):
        return (1 - am/g)*du_old + (am/(g*dt*af))*(u_naf - u_old)

    def update(old, mid, a):
        return old + (1/a)*(mid - old)

    # BCs
    #--------------------------------------------------------------------------------
    u_exact  = exact_solution(alpha, beta, t)
    du_exact = exact_du(alpha, beta, t)

    u_D = fem.Function(V)
    u_D.interpolate(u_exact)
    tdim = domain.topology.dim
    fdim = tdim - 1
    domain.topology.create_connectivity(fdim, tdim)
    boundary_facets = mesh.exterior_facet_indices(domain.topology)
    bc = fem.dirichletbc(u_D, fem.locate_dofs_topological(V, fdim, boundary_facets))

    # RHS function
    #--------------------------------------------------------------------------------
    f = fem.Constant(domain, beta - 2 - 2 * alpha)

    #--------------------------------------------------------------------------------
    # NONLINEAR SYSTEM SETUP WITH GEN ALPHA
    #--------------------------------------------------------------------------------

    # unknowns and intermediate variables
    u_naf  = fem.Function(V) # u at n + alpha_f
    u_n    = fem.Function(V)
    du_n   = fem.Function(V)
    u_np1  = fem.Function(V)
    du_np1 = fem.Function(V)
    du_nam = fem.Function(V) # du/dt at n + alpha_m
    
    # newmark formula
    u_np1.x.array[:] = u_n.x.array + dt*du_n.x.array + gamma*dt*(du_np1.x.array - du_n.x.array)
    du_nam.x.array[:] = correct_du(u_naf.x.array, u_n.x.array, du_n.x.array, alpha_m, alpha_f, gamma, dt)

    # test function
    v = ufl.TestFunction(V)
    Res = du_nam*v*ufl.dx + ufl.dot(ufl.grad(u_naf), ufl.grad(v))*ufl.dx - f*v*ufl.dx
    J = ufl.derivative(Res, u_naf, du_nam)

    # setup non-linear problem
    problem = fem.petsc.NonlinearProblem(Res,u_naf,[bc])
    solver  = nls.petsc.NewtonSolver(MPI.COMM_WORLD, problem)
    solver.convergence_criterion = "incremental"
    solver.rtol = 1e-6
    solver.report = False

    # initial conditions
    u_n.interpolate(u_exact)
    du_n.interpolate(du_exact)

    # time looping
    #--------------------------------------------------------------------------------

    for n in range(num_steps):
        # Update Diriclet boundary condition 
        u_exact.t+=alpha_f*dt # t at n+alpha_f
        u_D.interpolate(u_exact)

        # Solve NON-linear problem
        #--------------------------------------------------------------------------------

        # predict
        u_np1.x.array[:]  = predict_u(u_n.x.array)
        du_np1.x.array[:] = predict_du(du_n.x.array,gamma)

        # set intermediate values
        u_naf.x.array[:]  =  u_n.x.array + alpha_f*( u_np1.x.array -  u_n.x.array)
        du_nam.x.array[:] = du_n.x.array + alpha_m*(du_np1.x.array - du_n.x.array)

        # solver call to update u at n+alpha_f
        r = solver.solve(u_naf)

        # correct du/dt at n+alpha_m after Newton step
        du_nam.x.array[:] = correct_du(u_naf.x.array, u_n.x.array, du_n.x.array, alpha_m, alpha_f, gamma, dt)

        # update n+1
        u_np1.x.array[:]  = update( u_np1.x.array,  u_naf.x.array, alpha_f)
        du_np1.x.array[:] = update(du_np1.x.array, du_nam.x.array, alpha_m)

        # update for next iteration
        u_n.x.array[:]  =  u_np1.x.array
        du_n.x.array[:] = du_np1.x.array
        u_naf.x.scatter_forward()

        u_exact.t+=(-alpha_f*dt + dt)

        # error calcs
        V2 = fem.FunctionSpace(domain, ("CG", 3))
        uex = fem.Function(V2)
        uex.interpolate(u_exact)

        L2_error = fem.form(ufl.inner(u_n - uex, u_n - uex) * ufl.dx)
        error_local = fem.assemble_scalar(L2_error)
        error_L2 = numpy.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM))
        dof = len(u_naf.x.array)
        return error_L2, u_naf, dof
import numpy
# looping parameters & variables
#--------------------------------------------------------------------------------
rhos = numpy.array([0, 0.5, 1])
dxs = numpy.array([0.2, 0.1, 0.05, 0.025])

L2_Errors = numpy.empty([3,4])
dofs = numpy.empty([3,4])

# run convergece study
#--------------------------------------------------------------------------------

for outer in range(3):
    for inner in range(4):
        
        # indexing rho infinity and time step/mesh size
        rhoinf = rhos[outer]
        dt = dxs[inner]
        
        # run problem for convergence study
        error_L2,u_naf,dof = run_problem(dt, rhoinf)
        
        # save off L2-error and DOFs
        L2_Errors[outer][inner] = error_L2
        dofs[outer][inner] = dof # redundant
L2_Errors

My code now shows the expected behavior by implementing a custom newton solver