Computing derivative of residual vector w.r.t material parameter for finite element model updating

Hi all,

I am doing finite-element model updating for hyperelastic material models where I need analytic sensitivities of the residual w.r.t. constant material parameters:

\frac{\partial \mathbf{R}}{\partial m} \in \mathbb{R}^{N_u}

I do not use the adjoint method, so I need explicit parameter derivatives (implicit-function-theorem based optimization) and afterward solve a linear system with the Jacobian matrix to obtain the derivative of the displacement vector with repect to the material parameter:
\mathbf{J} \frac{\partial \mathbf{u}}{\partial m} = - \frac{\partial \mathbf{R}}{\partial m} , \quad \mathbf{K} = \frac{\partial \mathbf{R}}{\partial \mathbf{u}} \in \mathbb{R}^{N_u \times N_u}

Because my material model uses spline-based energies, I rely on dolfinx_external_operator. This introduces the constraint that material parameters (although constant) must be quadrature Functions, since external operators evaluate operands at quadrature points.

As a consequence, the only working pattern is:

Qe = basix.ufl.quadrature_element(
    domain.topology.cell_name(),
    degree=quadrature_degree,
    value_shape=()
)
Q = fem.functionspace(domain, Qe)

m = fem.Function(Q)  # Constant material parameter represented as a field
m.x.array[:] = 2.0

# Define external operator Psi and residual R in terms of m
# ...

dR_dm = ufl.derivative(R, m)
dR_dm_exp = ufl.algorithms.expand_derivatives(dR_dm)
R_repl, ops = replace_external_operators(dR_dm_exp)
A = assemble_matrix(fem.form(R_repl))  # <-- Matrix, not vector

Here, the assembly for dR/dm therefore gives a matrix. However, for a constant scalar parameter the mathematical object I actually want is a vector.

Questions

  1. Given the assembled matrix A above, what is the recommended way in FEniCSx to obtain the “physical” sensitivity vector dR/dm, for a constant material parameter represented as a quadrature Function?

  2. Is there any supported way (with FEMExternalOperator) to obtain dR/dm directly as a vector, without assembling the full Jacobian in parameter space? In my calibration problems I typically have 20–30 material parameters; storing 20–30 large matrices for all parameter sensitivities is quite expensive.

Minimal Code Example

The code below defines a variational problem for hyperelasticity with a simple linear energy density function expressed as external operator.

from mpi4py import MPI
import numpy as np
from petsc4py import PETSc
from dolfinx import *
import basix
import ufl
from petsc4py import PETSc
import dolfinx.fem as femx
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from dolfinx_external_operator import (
    FEMExternalOperator,
    replace_external_operators,
    evaluate_operands,
    evaluate_external_operators,
)

default_scalar_type = PETSc.ScalarType


def make_linear_iso_energy_external():
    """
    External operator representing dPsi/dI1 for a very simple
    isochoric energy Psi(I1_bar; slope) = slope * I1_bar
    """
    def psi_external(derivatives):
        # derivatives is (k_I1, k_slope)
        if derivatives == (0, 0):
            return lambda I1_vals, slope_vals: slope_vals.reshape(-1)
        elif derivatives == (1, 0):
            return lambda I1_vals, slope_vals: np.zeros_like(I1_vals).reshape(-1)
        elif derivatives == (0, 1):
            return lambda I1_vals, slope_vals: np.ones_like(slope_vals).reshape(-1)
        else:
            raise NotImplementedError(f"No implementation for derivatives={derivatives}")

    return psi_external


if __name__ == "__main__":
    comm = MPI.COMM_WORLD

    # Mesh and spaces
    domain = mesh.create_unit_cube(comm, 1, 1, 1)
    V = fem.functionspace(domain, ("CG", 1, (domain.geometry.dim,)))  # vector P1

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

    # Measure (quadrature of degree 3)
    quadrature_degree = 2
    dx = ufl.Measure(
        "dx",
        domain=domain,
        metadata={"quadrature_scheme": "default", "quadrature_degree": quadrature_degree},
    )

    # Kinematics
    d = domain.geometry.dim
    I = ufl.Identity(d)
    F = ufl.variable(I + ufl.grad(u))
    C = F.T * F
    J = ufl.det(F)
    I1 = ufl.tr(C)

    # Quadrature space for external operator and material parameter (slope)
    Qe = basix.ufl.quadrature_element(
        domain.topology.cell_name(),
        degree=quadrature_degree,
        value_shape=(),
    )
    Q = fem.functionspace(domain, Qe)

    # Slope parameter
    slope = fem.Function(Q)
    slope.x.array[:] = 5.0  # constant in space, but stored in quadrature field

    # External operator for dPsi/dI1_bar depending on two parameters
    dPsi_dI1 = FEMExternalOperator(I1, slope, function_space=Q)
    dPsi_dI1.external_function = make_linear_iso_energy_external()

    # Simple Piola stress: isochoric part via external operator + volumetric penalty
    P_iso = ufl.diff(I1, F) * dPsi_dI1
    P_vol = ufl.diff(J, F) * (10.0 * (J - 1.0))  # volumetric energy: 5*(J - 1)^2
    P = P_iso + P_vol

    # Weak form
    R = ufl.inner(P, ufl.grad(v)) * dx 

    # solve the forward problem (not shown here)
    # ...   
   
    # Residual w.r.t slope parameter
    dR_ds = ufl.derivative(R, slope)
    dR_ds_exp = ufl.algorithms.expand_derivatives(dR_ds) # Needed to expand derivatives of external operators
    dR_ds_replaced, dR_ds_ops = replace_external_operators(dR_ds_exp)
    dR_ds_form = femx.form(dR_ds_replaced)

    s_ops = evaluate_operands(dR_ds_ops)
    evaluate_external_operators(dR_ds_ops, s_ops)
    dR_dS_mat = femx.petsc.assemble_matrix(dR_ds_form)
    dR_dS_mat.assemble() # <-- Matrix
1 Like

Dear @SolidMechanicsFan,

Is there any supported way (with FEMExternalOperator) to obtain dR/dm directly as a vector, without assembling the full Jacobian in parameter space? In my calibration problems, I typically have 20–30 material parameters; storing 20–30 large matrices for all parameter sensitivities is quite expensive.

When you define the form, use u directly, not the test function

R = ufl.inner(P, ufl.grad(u)) * dx

Just pay attention to when to update u before or after assembling R, which is a vector now.

Given the assembled matrix A above, what is the recommended way in FEniCSx to obtain the “physical” sensitivity vector dR/dm, for a constant material parameter represented as a quadrature Function?

Looks OK to me, but I don’t have much experience with optimisation problems. @dokken Do you have any suggestions regarding the 1st question?

@SolidMechanicsFan
The pull request:

should make it possible to use the real function space, which can be found at:

together with external operator, where this would be the function_space of the operator.

@dokken

Perfect, “real” function space seems the right choice here.

together with external operator, where this would be the function_space of the operator.

Can you comment on what function_space you are referring to?

Example (below): Let the strain energy density be the external operator and the invariant I1 be its main operand. I pass the material parameters associated with the energy as additional operands. The function space of this operator is related to the quadrature points of the dx measure integrating the weak form. It can not be a real function space, as the energy can be different at every quadrature point of the domain.

Is the code below what you had in mind? Or would you suggest a different implementation?

# Real parameter space for scalar material parameter
R = create_real_functionspace(domain)
kappa = fem.Function(R)
kappa.x.array[:] = 5.0

# Quadrature space for external operator output 
Qe = basix.ufl.quadrature_element(
        domain.topology.cell_name(),
        degree=quadrature_degree,
        value_shape=(),
)
Q = fem.functionspace(domain, Qe)

# External operator for energy derivative
myOp = FEMExternalOperator(I1, kappa, function_space=Q)

I was thinking about the space that goes into the external operator.
This of course, only makes sense if and only if all inputs to the operator is from the real space.

In general, if you have spatial dependencies in the external operator (or other polynomial dependencies, you should use the real space for the slop input, while since I1 is spatially varying, you should still use Q for the dPsiDI1 operator.