PDEs can in general be subjected to a number of constraints,

\[g_I(\underline{a}) = 0, \quad I = 1 \text{ to } n_c\]

where $g$ are (non-linear) constraint equations, $\underline{a}$ is a vector of the degrees of freedom, and $n_c$ is the number of constraints. There are many ways to enforce these constraints, e.g. penalty methods and Lagrange multiplier methods.

Affine constraints

Affine or linear constraints can be handled directly in Ferrite. Such constraints can typically be expressed as:

\[a_1 = 5a_2 + 3a_3 + 1 \\ a_4 = 2a_3 + 6a_5 \\ \dots\]

where $a_1$, $a_2$ etc. are system degrees of freedom. In Ferrite, we can account for such constraint using the ConstraintHandler:

ch = ConstraintHandler(dh)
lc1 = AffineConstraint(1, [2 => 5.0, 3 => 3.0], 1)
lc2 = AffineConstraint(1, [3 => 2.0, 5 => 6.0], 0)
add!(ch, lc1)
add!(ch, lc2)

Affine constraints will affect the sparsity pattern of the stiffness matrix, and as such, it is important to also include the ConstraintHandler as an argument when creating the sparsity pattern:

K = create_sparsity_pattern(dh, ch)

Solving linear problems

To solve the system $\underline{\underline{K}}\underline{a}=\underline{f}$, account for affine constraints the same way as for Dirichlet boundary conditions; first call apply!(K, f, ch). This will condense K and f inplace (i.e no new matrix will be created). Note however that we must also call apply! on the solution vector after solving the system to enforce the affine constraints:

# ...
# Assemble K and f...

apply!(K, f, ch)
a = K\f
apply!(a, ch) # enforces affine constraints

Solving nonlinear problems

It is important to check the residual after applying boundary conditions when solving nonlinear problems with affine constraints. apply_zero!(K, r, ch) modifies the residual entries for dofs that are involved in constraints to account for constraint forces. The following pseudo-code shows a typical pattern for solving a non-linear problem with Newton's method:

a = initial_guess(...)  # Make any initial guess for a here, e.g. `a=zeros(ndofs(dh))`
apply!(a, ch)           # Make the guess fulfill all constraints in `ch`
for iter in 1:maxiter
    doassemble!(K, r, ...)  # Assemble the residual, r, and stiffness, K=∂r/∂a.
    apply_zero!(K, r, ch)   # Modify `K` and `r` to account for the constraints. 
    check_convergence(r, ...) && break # Only check convergence after `apply_zero!(K, r, ch)`
    Δa = K \ r              # Calculate the (negative) update
    apply_zero!(Δa, ch)     # Change the constrained values in `Δa` such that `a-Δa`
                            # fulfills constraints if `a` did.
    a .-= Δa