FEniCS/ffcx

#594 introduced a regression with certain forms

Closed this issue · 8 comments

Hi,
my RBniCSx nightly run failed tonight after the merge of #594, with the following error

libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c: In function ‘tabulate_tensor_integral_d0b4b45748c1730ae3264b6fe1db607b5018fa9a’:
libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c:813:13: error: lvalue required as decrement operand
  813 | sp_083[3] = --2 + c[1] / c[0];
      |             ^~
libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c: In function ‘tabulate_tensor_integral_cc5608c9f28da44e30c58e699b254e2dbcb55cb9’:
libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c:1038:13: error: lvalue required as decrement operand
 1038 | sp_083[3] = --2 + c[0] / c[1];
      |             ^~
libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c: In function ‘tabulate_tensor_integral_a1775329396972d86b0c0859f417536763fb104a’:
libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c:1263:13: error: lvalue required as decrement operand
 1263 | sp_083[3] = --2 + c[0] / c[1];
      |             ^~
libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c: In function ‘tabulate_tensor_integral_c4e485b50fad96fee23fe972761cfbebbc93f9a1’:
libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c:1488:13: error: lvalue required as decrement operand
 1488 | sp_083[3] = --2 + c[1] / c[0];
      |             ^~

The file libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c is attached (upon renaming to .c.txt, otherwise GitHub complains that the file format is not accepted)
libffcx_forms_a485dd25dfc109abba4e332f7d717e453f8b03b0.c.txt

If the cause of the regression is not immediately evident to you, can you help me reverse engineer which integral is being compiled at the failing lines?
The form I am using in the failing tutorials is very complicated

        # mu is dolfinx.fem.Constant with size 3
        thetas_a: typing.Tuple[  # type: ignore[no-any-unimported]
            typing.Callable[[rbnicsx.backends.SymbolicParameters], ufl.core.expr.Expr], ...
        ] = (
            # subdomains 1 and 7
            lambda mu: - (mu[1] - 2) / mu[0] - (2 * (2 * mu[0] - 2) * (mu[0] - 1)) / (mu[0] * (mu[1] - 2)),
            lambda mu: - mu[0] / (mu[1] - 2),
            lambda mu: - (2 * (mu[0] - 1)) / (mu[1] - 2),
            # subdomains 2 and 8
            lambda mu: 2 - (mu[0] - 1) * (mu[0] - 1) / (mu[1] - 2) - mu[1],
            lambda mu: - 1 / (mu[1] - 2),
            lambda mu: (mu[0] - 1) / (mu[1] - 2),
            # subdomains 3 and 5
            lambda mu: - mu[1] / (mu[0] - 2),
            lambda mu: - (mu[0] - 2) / mu[1] - (2 * (2 * mu[1] - 2) * (mu[1] - 1)) / (mu[1] * (mu[0] - 2)),
            lambda mu: - (2 * (mu[1] - 1)) / (mu[0] - 2),
            # subdomains 4 and 6
            lambda mu: - 1 / (mu[0] - 2),
            lambda mu: 2 - (mu[1] - 1) * (mu[1] - 1) / (mu[0] - 2) - mu[0],
            lambda mu: (mu[1] - 1) / (mu[0] - 2),
            # boundaries 5, 6, 7 and 8
            lambda mu: mu[2]
        )
        operators_a = (
            ufl.inner(u.dx(0), v.dx(0)) * dx(1) + ufl.inner(u.dx(0), v.dx(0)) * dx(7),
            ufl.inner(u.dx(1), v.dx(1)) * dx(1) + ufl.inner(u.dx(1), v.dx(1)) * dx(7),
            (ufl.inner(u.dx(0), v.dx(1)) * dx(1) + ufl.inner(u.dx(1), v.dx(0)) * dx(1)
             - ufl.inner(u.dx(0), v.dx(1)) * dx(7) - ufl.inner(u.dx(1), v.dx(0)) * dx(7)),
            # subdomains 2 and 8
            ufl.inner(u.dx(0), v.dx(0)) * dx(2) + ufl.inner(u.dx(0), v.dx(0)) * dx(8),
            ufl.inner(u.dx(1), v.dx(1)) * dx(2) + ufl.inner(u.dx(1), v.dx(1)) * dx(8),
            (ufl.inner(u.dx(0), v.dx(1)) * dx(2) + ufl.inner(u.dx(1), v.dx(0)) * dx(2)
             - ufl.inner(u.dx(0), v.dx(1)) * dx(8) - ufl.inner(u.dx(1), v.dx(0)) * dx(8)),
            # subdomains 3 and 5
            ufl.inner(u.dx(0), v.dx(0)) * dx(3) + ufl.inner(u.dx(0), v.dx(0)) * dx(5),
            ufl.inner(u.dx(1), v.dx(1)) * dx(3) + ufl.inner(u.dx(1), v.dx(1)) * dx(5),
            (ufl.inner(u.dx(0), v.dx(1)) * dx(3) + ufl.inner(u.dx(1), v.dx(0)) * dx(3)
             - ufl.inner(u.dx(0), v.dx(1)) * dx(5) - ufl.inner(u.dx(1), v.dx(0)) * dx(5)),
            # subdomains 4 and 6
            ufl.inner(u.dx(0), v.dx(0)) * dx(4) + ufl.inner(u.dx(0), v.dx(0)) * dx(6),
            ufl.inner(u.dx(1), v.dx(1)) * dx(4) + ufl.inner(u.dx(1), v.dx(1)) * dx(6),
            (ufl.inner(u.dx(0), v.dx(1)) * dx(4) + ufl.inner(u.dx(1), v.dx(0)) * dx(4)
             - ufl.inner(u.dx(0), v.dx(1)) * dx(6) - ufl.inner(u.dx(1), v.dx(0)) * dx(6)),
            # boundaries 5, 6, 7 and 8
            ufl.inner(u, v) * ds(5) + ufl.inner(u, v) * ds(6) + ufl.inner(u, v) * ds(7) + ufl.inner(u, v) * ds(8)
        )
        a = sum([theta_a(mu_symb) * operator_a for (theta_a, operator_a) in zip(thetas_a, operators_a)])
        dolfinx.fem.form(a)  # FAILS

I have attempted to simplify it to a minimally reproducible example, but any simpler example I could come up with (mixture of dx and ds, mixture of partial deriatives, coefficients with addends with a mix of positive/negative signs) did NOT show the issue.

I am quite confident the issue was introduced in #594 because CI ran ok yesterday night.

I will fix it today. I was expecting some issues to occur... unfortunately, our tests are not comprehensive enough...

This seems to be a minimal failing example:

mu = Constant(mesh, shape=(3,))
theta = - (mu[1] - 2) / mu[0] - (2 * (2 * mu[0] - 2) * (mu[0] - 1)) / (mu[0] * (mu[1] - 2))
a = theta * inner(grad(u), grad(v)) * dx

Also have issues with this PR with DOLFINX_MPC. Workflow ran yesterday, now yielding: https://github.com/jorgensd/dolfinx_mpc/actions/runs/6082097061/job/16499146654

This code also does not run the with the newest version of FFCx https://github.com/jorgensd/dolfinx-tutorial/blob/release/chapter2/hyperelasticity.py (Newton solver fails to converge, so I guess similar issue to what happens in MPC).

Thanks @jorgensd - I was expecting some failures, really. It is showing up inadequacies in the ffcx CI, which I will plug. I'll take a look at your failing examples.
I have now fixed the one for @francesco-ballarin in the branch https://github.com/FEniCS/ffcx/tree/chris/fix-issue-594

Code that runs on 0.6.0-r1 that works but fails with ffcx main (including the fix for issue 594):

import numpy as np
import ufl
from dolfinx import fem, log, mesh
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from mpi4py import MPI
from petsc4py import PETSc

L = 20.0
domain = mesh.create_box(MPI.COMM_WORLD, [[0.0, 0.0, 0.0], [L, 1, 1]], [20, 5, 5], mesh.CellType.hexahedron)
V = fem.VectorFunctionSpace(domain, ("Lagrange", 2))


# -

# We create two python functions for determining the facets to apply boundary conditions to

# +
def left(x):
    return np.isclose(x[0], 0)


def right(x):
    return np.isclose(x[0], L)


fdim = domain.topology.dim - 1
left_facets = mesh.locate_entities_boundary(domain, fdim, left)
right_facets = mesh.locate_entities_boundary(domain, fdim, right)
# -

# Next, we create a  marker based on these two functions

# Concatenate and sort the arrays based on facet indices. Left facets marked with 1, right facets with two
marked_facets = np.hstack([left_facets, right_facets])
marked_values = np.hstack([np.full_like(left_facets, 1), np.full_like(right_facets, 2)])
sorted_facets = np.argsort(marked_facets)
facet_tag = mesh.meshtags(domain, fdim, marked_facets[sorted_facets], marked_values[sorted_facets])

# We then create a function for supplying the boundary condition on the left side, which is fixed.

u_bc = np.array((0,) * domain.geometry.dim, dtype=PETSc.ScalarType)

# To apply the boundary condition, we identity the dofs located on the facets marked by the `MeshTag`.

left_dofs = fem.locate_dofs_topological(V, facet_tag.dim, facet_tag.find(1))
bcs = [fem.dirichletbc(u_bc, left_dofs, V)]

# Next, we define the body force on the reference configuration (`B`), and nominal (first Piola-Kirchhoff) traction (`T`).

B = fem.Constant(domain, PETSc.ScalarType((0, 0, 0)))
T = fem.Constant(domain, PETSc.ScalarType((0, 0, 0)))

# Define the test and solution functions on the space $V$

v = ufl.TestFunction(V)
u = fem.Function(V)

# Define kinematic quantities used in the problem

# +
# Spatial dimension
d = len(u)

# Identity tensor
I = ufl.variable(ufl.Identity(d))

# Deformation gradient
F = ufl.variable(I + ufl.grad(u))

# Right Cauchy-Green tensor
C = ufl.variable(F.T * F)

# Invariants of deformation tensors
Ic = ufl.variable(ufl.tr(C))
J = ufl.variable(ufl.det(F))
# -

# Define the elasticity model via a stored strain energy density function $\psi$, and create the expression for the first Piola-Kirchhoff stress:

# Elasticity parameters
E = PETSc.ScalarType(1.0e4)
nu = PETSc.ScalarType(0.3)
mu = fem.Constant(domain, PETSc.ScalarType(E / (2 * (1 + nu))))
lmbda = fem.Constant(domain, PETSc.ScalarType(E * nu / ((1 + nu) * (1 - 2 * nu))))
# Stored strain energy density (compressible neo-Hookean model)
psi = (mu / 2) * (Ic - 3) - mu * ufl.ln(J) + (lmbda / 2) * (ufl.ln(J))**2
# Stress
# Hyper-elasticity
P = ufl.diff(psi, F)

# ```{admonition} Comparison to linear elasticity
# To illustrate the difference between linear and hyperelasticity, the following lines can be uncommented to solve the linear elasticity problem.
# ```

# +
# P = 2.0 * mu * ufl.sym(ufl.grad(u)) + lmbda * ufl.tr(ufl.sym(ufl.grad(u))) * I
# -

# Define the variational form with traction integral over all facets with value 2. We set the quadrature degree for the integrals to 4.

metadata = {"quadrature_degree": 4}
ds = ufl.Measure('ds', domain=domain, subdomain_data=facet_tag, metadata=metadata)
dx = ufl.Measure("dx", domain=domain, metadata=metadata)
# Define form F (we want to find u such that F(u) = 0)
F = ufl.inner(ufl.grad(v), P) * dx - ufl.inner(v, B) * dx - ufl.inner(v, T) * ds(2)

# As the varitional form is non-linear and written on residual form, we use the non-linear problem class from DOLFINx to set up required structures to use a Newton solver.

problem = NonlinearProblem(F, u, bcs)

# and then create and customize the Newton solver

# +
solver = NewtonSolver(domain.comm, problem)

# Set Newton solver options
solver.atol = 1e-8
solver.rtol = 1e-8
solver.convergence_criterion = "incremental"


# Finally, we solve the problem over several time steps, updating the y-component of the traction

log.set_log_level(log.LogLevel.INFO)
tval0 = -1.5
for n in range(1, 10):
    T.value[2] = n * tval0
    num_its, converged = solver.solve(u)
    assert (converged)
    u.x.scatter_forward()
    print(f"Time step {n}, Number of iterations {num_its}, Load {T.value}")

How does it fail? I am getting:

Time step 1, Number of iterations 8, Load [ 0.   0.  -1.5]
Time step 2, Number of iterations 9, Load [ 0.  0. -3.]
Time step 3, Number of iterations 10, Load [ 0.   0.  -4.5]
Time step 4, Number of iterations 9, Load [ 0.  0. -6.]
Time step 5, Number of iterations 8, Load [ 0.   0.  -7.5]
Time step 6, Number of iterations 7, Load [ 0.  0. -9.]
Time step 7, Number of iterations 6, Load [  0.    0.  -10.5]
Time step 8, Number of iterations 6, Load [  0.   0. -12.]
Time step 9, Number of iterations 6, Load [  0.    0.  -13.5]

How does it fail? I am getting:


Time step 1, Number of iterations 8, Load [ 0.   0.  -1.5]

Time step 2, Number of iterations 9, Load [ 0.  0. -3.]

Time step 3, Number of iterations 10, Load [ 0.   0.  -4.5]

Time step 4, Number of iterations 9, Load [ 0.  0. -6.]

Time step 5, Number of iterations 8, Load [ 0.   0.  -7.5]

Time step 6, Number of iterations 7, Load [ 0.  0. -9.]

Time step 7, Number of iterations 6, Load [  0.    0.  -10.5]

Time step 8, Number of iterations 6, Load [  0.   0. -12.]

Time step 9, Number of iterations 6, Load [  0.    0.  -13.5]

I Get that the newton solver never reaches convergence. This happens on my CI (and locally) https://github.com/jorgensd/dolfinx-tutorial/actions/runs/6082502881/job/16500423176