Repository: Ferrite-FEM/Ferrite.jl
Pull request #1151: ProjectedDirichlet BC for H(div) and H(curl)
Author: login: KnutAM
Created: 2025-02-14T19:04:53Z | Merged: 2025-04-30T13:34:27Z

For H(div) and H(curl) interpolations, the dof is not associated with a point, but with facets and edges, respectively. However, the boundary conditions appear for the facets in the forms described in the [docstring](https://ferrite-fem.github.io/Ferrite.jl/previews/PR1151/reference/boundary_conditions/#Ferrite.WeakDirichlet). This implies that we cannot get a coordinate where we want to assign the dof, but instead we have to minimize the difference, not sure how much theory to include, but we could add for e.g. H(div) that our "problem" is,
```math
\begin{align*}
&\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} = f \quad \text{ on } \Gamma, \quad \text{Not possible for arbitrary } f(\boldsymbol{x},...) \\
&\text{Instead}\\
&\boldsymbol{q}(\boldsymbol{x}) = \mathrm{argmin} U(\boldsymbol{q}(\boldsymbol{x})), \quad U(\boldsymbol{q}(\boldsymbol{x})) = \int_\Gamma \left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f\right]^2\ \mathrm{d}\Gamma \\
&\text{Found as solution to the directional derivative in all possible directions,}\delta\boldsymbol{q}(\boldsymbol{x})\text{. I.e., for }\epsilon \rightarrow 0, \\
&0 = \frac{\partial}{\partial \epsilon} U(\boldsymbol{q}(\boldsymbol{x}) + \epsilon \delta\boldsymbol{q}(\boldsymbol{x}) = \int_\Gamma \left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f\right]\delta\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n}\ \mathrm{d}\Gamma \\ 
&\text{Introducing the FE approximation for test and trial, we then get what we have in the docstring},\\
&\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right]\left[\boldsymbol{N}^f_j \cdot \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right] q_n\ \mathrm{d}\Gamma}_{f^f_i}
\end{align*}
```
where the functional dependence, $f = f(\boldsymbol{x}, t, \boldsymbol{n})$, is skipped fore brevity. 

I also chose to introduce the internal `function_space(::Interpolation)::Val` function, as this was useful for dispatching. @termi-official: `RannacherTurek` and `CrouzeixRaviart` should be `L2`, right? 

<details> <summary> Side-note</summary>
An alternative name could be L2Dirichlet, as this could also be applied to $H^1$ spaces as well, and then it would actually be more accurate in the L2 sense. The problem with implementing that is that we get contributions from neighboring facets, and thus it either becomes a challenging problem to implement, or we need to solve a boundary FE problem on the global scale (probably easier and not so bad considering the sparsity) in order to apply the boundary conditions. But in most (probably all) practical cases, the additional "accuracy" in applying the BC is offset by the fact that an FE solution wouldn't be accurate enough if resolving the actual shape of the boundary prescribing function is important. However, if using $H^1$ spaces to describe e.g. fluxes, then using a standard Dirichlet condition would give the wrong total flux value if one uses e.g. linear shape functions but prescribe a quadratic velocity profile (e.g. in fluid problems...). 
</details>


Diff:
diff --git a/.gitignore b/.gitignore
index ecbf08c7fb..07fd400412 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ docs/src/examples/
 docs/src/tutorials/
 docs/src/howto/
 docs/src/gallery/
+docs/src/topics/downloaded_assets/
 docs/src/changelog.md
 *.DS_Store
 /Manifest.toml
diff --git a/docs/download_resources.jl b/docs/download_resources.jl
index 1a78b46f7f..b084f25807 100644
--- a/docs/download_resources.jl
+++ b/docs/download_resources.jl
@@ -1,6 +1,7 @@
 # Download some assets necessary for docs/testing not stored in the repo
 import Downloads
 
+# Tutorials
 const directory = joinpath(@__DIR__, "src", "tutorials")
 mkpath(directory)
 
@@ -21,3 +22,14 @@ for (file, url) in [
         Downloads.download(url, afile)
     end
 end
+
+# Topics
+const topics_directory = joinpath(@__DIR__, "src", "topics", "downloaded_assets")
+mkpath(topics_directory)
+
+for (file, url) in [
+        "ProjectedDirichlet.svg" => "https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/ProjectedDirichlet.svg",
+    ]
+    afile = joinpath(topics_directory, file)
+    isfile(afile) || Downloads.download(url, afile)
+end
diff --git a/docs/src/assets/references.bib b/docs/src/assets/references.bib
index c240c43833..8980941128 100644
--- a/docs/src/assets/references.bib
+++ b/docs/src/assets/references.bib
@@ -190,3 +190,13 @@ @article{CroRav:1973:cnf
   year={1973},
   publisher={EDP Sciences}
 }
+@article{Bartels2004:ProjectedDirichlet,
+author = {Bartels, S. and Carstensen, C. and Dolzmann, G.},
+doi = {10.1007/s00211-004-0548-3},
+journal = {Numerische Mathematik},
+number = {1},
+pages = {1--24},
+title = {{Inhomogeneous Dirichlet conditions in a priori and a posteriori finite element error analysis}},
+volume = {99},
+year = {2004}
+}
diff --git a/docs/src/reference/boundary_conditions.md b/docs/src/reference/boundary_conditions.md
index 2908ba4775..8d0cbe4300 100644
--- a/docs/src/reference/boundary_conditions.md
+++ b/docs/src/reference/boundary_conditions.md
@@ -11,6 +11,7 @@ Pages = ["boundary_conditions.md"]
 ```@docs
 ConstraintHandler
 Dirichlet
+ProjectedDirichlet
 PeriodicDirichlet
 collect_periodic_facets
 collect_periodic_facets!
diff --git a/docs/src/topics/boundary_conditions.md b/docs/src/topics/boundary_conditions.md
index 7f70ea671c..11e3c09a3c 100644
--- a/docs/src/topics/boundary_conditions.md
+++ b/docs/src/topics/boundary_conditions.md
@@ -93,6 +93,22 @@ end
     Most examples make use of Dirichlet boundary conditions, for example [Heat
     Equation](@ref tutorial-heat-equation).
 
+## ProjectedDirichlet
+Some interpolations don't have nodal support points:
+``H(\mathrm{curl})`` interpolations, e.g., `Nedelec`, are associated to edges and faces,
+while ``H(\mathrm{div})`` interpolations, e.g. `RaviartThomas`, are associated to facets.
+While normal `Dirichlet` boundary conditions assume the existence of such nodal support points,
+Ferrite provides the `ProjectedDirichlet`, which instead finds the degree of freedom values that
+minimizes the L2-distance between the prescribed function, ``f(\boldsymbol{x},t,\boldsymbol{n})``,
+and the finite element interpolation space, cf. [Bartels2004:ProjectedDirichlet](@cite).
+Although standard interpolations are not currently supported,
+the figure below illustrates well the difference between applying a standard `Dirichlet` condition and an
+`ProjectedDirichlet` condition when the prescribed function cannot be described by the chosen FE-interpolation.
+
+![ProjectedDirichlet illustration](downloaded_assets/ProjectedDirichlet.svg)
+
+Here, we note that while the `Dirichlet` condition gives the correct value at the nodes, the `ProjectedDirichlet` gives
+a more accurate average boundary value (specifically the L2 projection of `f(x)` onto the finite element space).
 
 ## Neumann boundary conditions
 At the Neumann part of the boundary we know something about the gradient of the solution.
diff --git a/src/Dofs/ConstraintHandler.jl b/src/Dofs/ConstraintHandler.jl
index a48cb3940f..028966daf5 100644
--- a/src/Dofs/ConstraintHandler.jl
+++ b/src/Dofs/ConstraintHandler.jl
@@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    ProjectedDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
+
+A `ProjectedDirichlet` condition enforces conditions for `field_name` on the boundary, `facets`, for
+non-nodal interpolations (e.g. ``H(\mathrm{div})`` or ``H(\mathrm{curl})``) by
+minimizing the L2 error between the function `f(x, t, n)` and the values described
+by the FE approximation on the facet, ``\Gamma^f``. The arguments to the function are
+the coordinate, ``\boldsymbol{x}``, the time, ``t``, and the facet normal, ``\boldsymbol{n}``.
+The quadrature rule is automatically created, but the default order, `qr_order = 2 * ip_order`
+(may be refined in future releases),
+where `ip_order` is the order of the interpolation, can be overrided if desired.
+
+# H(div) interpolations
+For H(div), we want to prescribe the normal flux, ``q_\mathrm{n} = f(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n}) \right]^2 \mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Finding the stationary point by taking the directional derivative in all possible directions,
+``\delta\boldsymbol{q}(\boldsymbol{x})``, yields,
+```math
+0 = \lim_{\epsilon\rightarrow 0}\frac{\partial}{\partial \epsilon}
+U\big(\boldsymbol{q}(\boldsymbol{x}) + \epsilon \delta\boldsymbol{q}(\boldsymbol{x})\big)
+ = 2\int_{\Gamma^f} \left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n})\right]
+    \delta\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n}\ \mathrm{d}\Gamma
+```
+Inserting the FE-approximations,
+``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f`` and
+``\delta\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_i(\boldsymbol{x}) c_i^f``,
+and using the arbitrariness of ``c_i^f``, yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right]\left[\boldsymbol{N}^f_j \cdot \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right] f(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``.
+
+# H(curl) interpolations
+For H(curl), we want to prescribe the tangential flux, ``\boldsymbol{q}_\mathrm{t} = \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left\vert\left\vert
+    \boldsymbol{q}(\boldsymbol{x}) \times \boldsymbol{n} - \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})
+\right\vert\right\vert^2
+\mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Similar to for ``H(\mathrm{div})``, we find the minimum by setting the directional derivative equal to zero,
+which after inserting the FE-approximations yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot\left[\boldsymbol{N}^f_j \times \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``
+"""
+mutable struct ProjectedDirichlet
+    const f::Function
+    const facets::OrderedSet{FacetIndex}
+    const field_name::Symbol
+    const qr_order::Int
+    # Created during `add!`
+    fv::Union{Nothing, FacetValues}
+    facet_dofs::Union{Nothing, ArrayOfVectorViews{Int, 1}}
+end
+function ProjectedDirichlet(field_name::Symbol, facets::AbstractVecOrSet, f::Function; qr_order = -1)
+    return ProjectedDirichlet(f, convert_to_orderedset(facets), field_name, qr_order, nothing, nothing)
+end
+
 const DofCoefficients{T} = Vector{Pair{Int, T}}
 """
     AffineConstraint(constrained_dof::Int, entries::Vector{Pair{Int,T}}, b::T) where T
@@ -83,6 +155,7 @@ A collection of constraints associated with the dof handler `dh`.
 """
 mutable struct ConstraintHandler{DH <: AbstractDofHandler, T}
     const dbcs::Vector{Dirichlet}
+    const projbcs::Vector{ProjectedDirichlet}
     const prescribed_dofs::Vector{Int}
     const free_dofs::Vector{Int}
     const inhomogeneities::Vector{T}
@@ -103,8 +176,8 @@ ConstraintHandler(dh::AbstractDofHandler) = ConstraintHandler(Float64, dh)
 function ConstraintHandler(::Type{T}, dh::AbstractDofHandler) where {T <: Number}
     @assert isclosed(dh)
     return ConstraintHandler(
-        Dirichlet[], Int[], Int[], T[], Union{Nothing, T}[], Union{Nothing, DofCoefficients{T}}[],
-        Dict{Int, Int}(), BCValues{T}[], dh, false,
+        Dirichlet[], ProjectedDirichlet[], Int[], Int[], T[], Union{Nothing, T}[],
+        Union{Nothing, DofCoefficients{T}}[], Dict{Int, Int}(), BCValues{T}[], dh, false,
     )
 end
 
@@ -412,10 +485,13 @@ function update!(ch::ConstraintHandler, time::Real = 0.0)
         wrapper_f = hasmethod(dbc.f, Tuple{get_coordinate_type(get_grid(ch.dh)), typeof(time)}) ? dbc.f : (x, _) -> dbc.f(x)
         # Function barrier
         _update!(
-            ch.inhomogeneities, wrapper_f, dbc.facets, dbc.field_name, dbc.local_facet_dofs, dbc.local_facet_dofs_offset,
+            ch.inhomogeneities, wrapper_f, dbc.facets, dbc.local_facet_dofs, dbc.local_facet_dofs_offset,
             dbc.components, ch.dh, ch.bcvalues[i], ch.dofmapping, ch.dofcoefficients, time
         )
     end
+    for bc in ch.projbcs
+        _update_projected_dbc!(ch.inhomogeneities, bc.f, bc.facets, bc.fv, bc.facet_dofs, ch.dh, ch.dofmapping, ch.dofcoefficients, time)
+    end
     # Compute effective inhomogeneity for affine constraints with prescribed dofs in the
     # RHS. For example, in u2 = w3 * u3 + w4 * u4 + b2 we allow e.g. u3 to be prescribed by
     # a trivial constraint with just an inhomogeneity (e.g. DBC), for example u3 = f(t).
@@ -440,7 +516,7 @@ end
 
 # for facets, vertices, faces and edges
 function _update!(
-        inhomogeneities::Vector{T}, f::Function, boundary_entities::AbstractVecOrSet{<:BoundaryIndex}, field::Symbol, local_facet_dofs::Vector{Int}, local_facet_dofs_offset::Vector{Int},
+        inhomogeneities::Vector{T}, f::Function, boundary_entities::AbstractVecOrSet{<:BoundaryIndex}, local_facet_dofs::Vector{Int}, local_facet_dofs_offset::Vector{Int},
         components::Vector{Int}, dh::AbstractDofHandler, boundaryvalues::BCValues,
         dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
     ) where {T}
@@ -480,7 +556,7 @@ end
 
 # for nodes
 function _update!(
-        inhomogeneities::Vector{T}, f::Function, ::AbstractVecOrSet{Int}, field::Symbol, nodeidxs::Vector{Int}, globaldofs::Vector{Int},
+        inhomogeneities::Vector{T}, f::Function, ::AbstractVecOrSet{Int}, nodeidxs::Vector{Int}, globaldofs::Vector{Int},
         components::Vector{Int}, dh::AbstractDofHandler, facetvalues::BCValues,
         dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
     ) where {T}
@@ -1762,3 +1838,150 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, bc::ProjectedDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        bc.field_name in sdh.field_names || continue
+        # Compute the intersection between bc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, bc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, bc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(bc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = ProjectedDirichlet(bc.f, filtered_set, bc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between bc::ProjectedDirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, bc::ProjectedDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in bc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.projbcs, bc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_projected_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = get_base_interpolation(function_interpolation(fv)) # Ensures getting error message from `integrate_projected_dbc!`
+    max_dofs_per_facet = maximum(length, dirichlet_facetdof_indices(ip))
+    Kᶠ = zeros(max_dofs_per_facet, max_dofs_per_facet)
+    aᶠ = zeros(max_dofs_per_facet)
+    fᶠ = zeros(max_dofs_per_facet)
+    for fc in FacetIterator(dh, facets)
+        reinit!(fv, fc)
+        shape_nrs = dirichlet_facetdof_indices(ip)[getcurrentfacet(fv)]
+        solve_projected_dbc!(aᶠ, Kᶠ, fᶠ, f, fv, shape_nrs, getcoordinates(fc), time)
+        for (idof, shape_nr) in enumerate(shape_nrs)
+            globaldof = celldofs(fc)[shape_nr]
+            dbc_index = dofmapping[globaldof]
+            # Only DBC dofs are currently update!-able so don't modify inhomogeneities
+            # for affine constraints
+            if dofcoefficients[dbc_index] === nothing
+                inhomogeneities[dbc_index] = aᶠ[idof]
+            end
+        end
+    end
+    return nothing
+end
+
+function solve_projected_dbc!(aᶠ, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    fill!(Kᶠ, 0)
+    fill!(fᶠ, 0)
+    # Supporting varying number of facetdofs (for ref shapes with different facet types) requires
+    # for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
+    #     Kᶠ[i, i] = 1
+    # end
+    @assert length(shape_nrs) == size(Kᶠ, 1)
+    integrate_projected_dbc!(Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    aᶠ .= Kᶠ \ fᶠ # Could be done non-allocating if required, using e.g. SMatrix
+    return aᶠ
+end
+
+function integrate_projected_dbc!(Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    return integrate_projected_dbc!(conformity(fv), Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+end
+
+function integrate_projected_dbc!(::HdivConformity, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    for q_point in 1:getnquadpoints(fv)
+        dΓ = getdetJdV(fv, q_point)
+        n = getnormal(fv, q_point)
+        x = spatial_coordinate(fv, q_point, cell_coords)
+        qn = bc_fun(x, time, n)
+        for (i, I) in enumerate(shape_nrs)
+            δN_dot_n = shape_value(fv, q_point, I) ⋅ n
+            fᶠ[i] += qn * δN_dot_n * dΓ
+            for (j, J) in enumerate(shape_nrs)
+                N_dot_n = shape_value(fv, q_point, J) ⋅ n
+                Kᶠ[i, j] += (δN_dot_n * N_dot_n) * dΓ
+            end
+        end
+    end
+    return
+end
+
+function integrate_projected_dbc!(::HcurlConformity, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    if getrefdim(function_interpolation(fv)) == 3
+        throw(ArgumentError("ProjectedDirichlet is not implemented for 3D H(curl) conformity"))
+    end
+    for q_point in 1:getnquadpoints(fv)
+        dΓ = getdetJdV(fv, q_point)
+        n = getnormal(fv, q_point)
+        x = spatial_coordinate(fv, q_point, cell_coords)
+        qt = bc_fun(x, time, n)
+        for (i, I) in enumerate(shape_nrs)
+            δN_cross_n = shape_value(fv, q_point, I) × n
+            fᶠ[i] += (δN_cross_n ⋅ qt) * dΓ
+            for (j, J) in enumerate(shape_nrs)
+                N_cross_n = shape_value(fv, q_point, J) × n
+                Kᶠ[i, j] += (δN_cross_n ⋅ N_cross_n) * dΓ
+            end
+        end
+    end
+    return
+end
+
+function integrate_projected_dbc!(::Union{H1Conformity, L2Conformity}, _, _, _, fv, args...)
+    ip_str = sprint(show, function_interpolation(fv))
+    throw(ArgumentError("ProjectedDirichlet is not implemented for H¹ and L2 conformities ($ip_str)"))
+end
diff --git a/src/FEValues/common_values.jl b/src/FEValues/common_values.jl
index dc56a2d42e..a330ba9414 100644
--- a/src/FEValues/common_values.jl
+++ b/src/FEValues/common_values.jl
@@ -30,6 +30,8 @@ end
     throw(ArgumentError(msg))
 end
 
+conformity(fe_values::AbstractValues) = conformity(function_interpolation(fe_values))
+
 """
     ValuesUpdateFlags(ip_fun::Interpolation; update_gradients = Val(true), update_hessians = Val(false), update_detJdV = Val(true))
 
diff --git a/src/exports.jl b/src/exports.jl
index 786fbf8b29..bb5309b97c 100644
--- a/src/exports.jl
+++ b/src/exports.jl
@@ -140,6 +140,7 @@ export
     # Constraints
     ConstraintHandler,
     Dirichlet,
+    ProjectedDirichlet,
     PeriodicDirichlet,
     collect_periodic_facets,
     collect_periodic_facets!,
diff --git a/src/interpolations.jl b/src/interpolations.jl
index ee78671f3e..a465ce2ae7 100644
--- a/src/interpolations.jl
+++ b/src/interpolations.jl
@@ -51,6 +51,11 @@ const InterpolationByDim{dim} = Interpolation{<:AbstractRefShape{dim}}
 abstract type ScalarInterpolation{refshape, order} <: Interpolation{refshape, order} end
 abstract type VectorInterpolation{vdim, refshape, order} <: Interpolation{refshape, order} end
 
+struct L2Conformity end     # Discontinuous across cell boundaries
+struct HdivConformity end   # Normal continuity across cell boundaries
+struct HcurlConformity end  # Tangent continuity across cell boundaries
+struct H1Conformity end     # Function continuity across cell boundaries
+
 # Number of components for the interpolation.
 n_components(::ScalarInterpolation) = 1
 n_components(::VectorInterpolation{vdim}) where {vdim} = vdim
@@ -473,6 +478,7 @@ struct DiscontinuousLagrange{shape, order} <: ScalarInterpolation{shape, order}
         return new{shape, order}()
     end
 end
+conformity(::DiscontinuousLagrange) = L2Conformity()
 
 adjust_dofs_during_distribution(::DiscontinuousLagrange) = false
 
@@ -530,6 +536,7 @@ struct Lagrange{shape, order} <: ScalarInterpolation{shape, order}
         return new{shape, order}()
     end
 end
+conformity(::Lagrange) = H1Conformity()
 
 adjust_dofs_during_distribution(::Lagrange) = true
 adjust_dofs_during_distribution(::Lagrange{<:Any, 2}) = false
@@ -1342,6 +1349,7 @@ struct BubbleEnrichedLagrange{shape, order} <: ScalarInterpolation{shape, order}
         return new{shape, order}()
     end
 end
+conformity(::BubbleEnrichedLagrange) = H1Conformity()
 
 #######################################
 # Lagrange-Bubble RefTriangle order 1 #
@@ -1387,6 +1395,7 @@ struct Serendipity{shape, order} <: ScalarInterpolation{shape, order}
         return new{shape, order}()
     end
 end
+conformity(::Serendipity) = H1Conformity()
 
 # Note that the edgedofs for high order serendipity elements are defined in terms of integral moments,
 # so no permutation exists in general. See e.g. Scroggs et al. [2022] for an example.
@@ -1537,6 +1546,7 @@ struct CrouzeixRaviart{shape, order} <: ScalarInterpolation{shape, order}
     CrouzeixRaviart{RefTriangle, 1}() = new{RefTriangle, 1}()
     CrouzeixRaviart{RefTetrahedron, 1}() = new{RefTetrahedron, 1}()
 end
+conformity(::CrouzeixRaviart) = L2Conformity()
 
 # CR elements are characterized by not having vertex dofs
 vertexdof_indices(ip::CrouzeixRaviart) = ntuple(i -> (), nvertices(ip))
@@ -1605,6 +1615,7 @@ This element is basically the idea from Crouzeix and Raviart applied to
 hypercubes. For details see the original paper [RanTur:1992:snq](@cite).
 """
 struct RannacherTurek{shape, order} <: ScalarInterpolation{shape, order} end
+conformity(::RannacherTurek) = L2Conformity()
 
 # CR-type elements are characterized by not having vertex dofs
 vertexdof_indices(ip::RannacherTurek) = ntuple(i -> (), nvertices(ip))
@@ -1683,6 +1694,7 @@ struct VectorizedInterpolation{vdim, refshape, order, SI <: ScalarInterpolation{
         return new{vdim, refshape, order, SI}(ip)
     end
 end
+conformity(ip::VectorizedInterpolation) = conformity(ip.ip)
 
 adjust_dofs_during_distribution(ip::VectorizedInterpolation) = adjust_dofs_during_distribution(ip.ip)
 
@@ -1826,6 +1838,7 @@ struct RaviartThomas{shape, order, vdim} <: VectorInterpolation{vdim, shape, ord
     end
 end
 mapping_type(::RaviartThomas) = ContravariantPiolaMapping()
+conformity(::RaviartThomas) = HdivConformity()
 
 # RefTriangle
 edgedof_indices(ip::RaviartThomas{RefTriangle}) = edgedof_interior_indices(ip)
@@ -1964,6 +1977,7 @@ struct BrezziDouglasMarini{shape, order, vdim} <: VectorInterpolation{vdim, shap
     end
 end
 mapping_type(::BrezziDouglasMarini) = ContravariantPiolaMapping()
+conformity(::BrezziDouglasMarini) = HdivConformity()
 
 # RefTriangle
 edgedof_indices(ip::BrezziDouglasMarini{RefTriangle}) = edgedof_interior_indices(ip)
@@ -2003,6 +2017,7 @@ struct Nedelec{shape, order, vdim} <: VectorInterpolation{vdim, shape, order}
     end
 end
 mapping_type(::Nedelec) = CovariantPiolaMapping()
+conformity(::Nedelec) = HcurlConformity()
 edgedof_indices(ip::Nedelec) = edgedof_interior_indices(ip)
 
 # 2D refshape (rdim == vdim for Nedelec)
diff --git a/test/test_continuity.jl b/test/test_continuity.jl
index 730840d8bb..8723bd1e85 100644
--- a/test/test_continuity.jl
+++ b/test/test_continuity.jl
@@ -113,11 +113,10 @@
         return (Tetrahedron(ntuple(i -> cell.nodes[perm[i]], 4)) for perm in idx)
     end
 
-    continuity_function(ip::VectorizedInterpolation) = continuity_function(ip.ip)
-    continuity_function(::Lagrange) = ((v, _) -> v)
-    continuity_function(::Nedelec) = ((v, n) -> v - n * (v ⋅ n)) # Tangent continuity (H(curl))
-    continuity_function(::RaviartThomas) = ((v, n) -> v ⋅ n) # Normal continuity (H(div))
-    continuity_function(::BrezziDouglasMarini) = ((v, n) -> v ⋅ n) # Normal continuity (H(div))
+    continuity_function(ip::Interpolation) = continuity_function(Ferrite.conformity(ip))
+    continuity_function(::Ferrite.H1Conformity) = ((v, _) -> v)
+    continuity_function(::Ferrite.HcurlConformity) = ((v, n) -> v - n * (v ⋅ n)) # Tangent continuity
+    continuity_function(::Ferrite.HdivConformity) = ((v, n) -> v ⋅ n) # Normal continuity
 
     nel = 3
 
diff --git a/test/test_interpolations.jl b/test/test_interpolations.jl
index 0cce369344..65ae743d7e 100644
--- a/test/test_interpolations.jl
+++ b/test/test_interpolations.jl
@@ -20,6 +20,7 @@ C) Lower-dimensional entities' dof indices + current interior dof indices
 D) The dof indices values matches `1:N` without duplication (follows from B, but also checked separately)
 E) All `N` base functions are implemented + `ArgumentError` if `i=0` or `i=N+1`
 F) Interpolation accessor functions versus type parameters (e.g. same refshape)
+G) `conformity` and `mapping_type` is defined
 """
 function test_interpolation_properties(ip::Interpolation{RefShape, FunOrder}) where {RefShape, FunOrder}
     return @testset "Interpolation properties: $ip" begin
@@ -62,6 +63,10 @@ function test_interpolation_properties(ip::Interpolation{RefShape, FunOrder}) wh
             end
             @test_throws ArgumentError Ferrite.reference_shape_value(ip, ξ, getnbasefunctions(ip) + 1)
         end
+
+        # Test that property functions are defined, runs, and, if possible, give expected type
+        Ferrite.mapping_type(ip) # Dry-run just to catch if it isn't defined
+        @test Ferrite.conformity(ip) isa Union{Ferrite.L2Conformity, Ferrite.HdivConformity, Ferrite.HcurlConformity, Ferrite.H1Conformity}
     end
 end
 
@@ -395,12 +400,6 @@ end
         ## Nedelec on RefHexahedron
         reference_edge_moment(::Nedelec{RefHexahedron, 1}, edge_shape_nr, s) = 1
 
-        function_space(::RaviartThomas) = Val(:Hdiv)
-        function_space(::BrezziDouglasMarini) = Val(:Hdiv)
-        function_space(::Nedelec) = Val(:Hcurl)
-        # function_space(::Lagrange) = Val(:H1)
-        # function_space(::DiscontinuousLagrange) = Val(:L2)
-
         """
             integrate_edge(f)
 
@@ -436,19 +435,19 @@ end
             end
         end
 
-        # parameterize_volume(volume_coords, s1, s2, s3) = # TODO parameterization of volume (Tetrahedron, Hexahedron, Prism, Pyramid)
+        # parameterize_volume(volume_coords, s1, s2, s3)
+        # TODO parameterization of volume (Tetrahedron, Hexahedron, Prism, Pyramid)
 
         edge_weight(ξ::F, s) where {F <: Function} = norm(gradient(ξ, s))
         face_weight(ξ::F, s1, s2) where {F <: Function} = norm(gradient(s -> ξ(s, s2), s1) × gradient(s -> ξ(s1, s), s2))
 
         function test_interpolation_functionals(ip::Interpolation)
             @testset "functionals $ip" begin
-                test_interpolation_functionals(function_space(ip), Val(Ferrite.getrefdim(ip)), ip)
+                test_interpolation_functionals(Ferrite.conformity(ip), Val(Ferrite.getrefdim(ip)), ip)
             end
         end
 
-        # 2D, H(div)
-        function test_interpolation_functionals(::Val{:Hdiv}, ::Val{2}, ip::Interpolation)
+        function test_interpolation_functionals(::Ferrite.HdivConformity, ::Val{2}, ip::Interpolation)
             RefShape = getrefshape(ip)
             ipg = Lagrange{RefShape, 1}()
 
@@ -501,7 +500,7 @@ end
         end
 
         # 3D, H(div)
-        function test_interpolation_functionals(::Val{:Hdiv}, ::Val{3}, ip::Interpolation)
+        function test_interpolation_functionals(::Ferrite.HdivConformity, ::Val{3}, ip::Interpolation)
             RefShape = getrefshape(ip)
             ipg = Lagrange{RefShape, 1}()
 
@@ -539,7 +538,7 @@ end
         end
 
         # H(curl)
-        function test_interpolation_functionals(::Val{:Hcurl}, ::Union{Val{2}, Val{3}}, ip::Interpolation)
+        function test_interpolation_functionals(::Ferrite.HcurlConformity, ::Union{Val{2}, Val{3}}, ip::Interpolation)
             RefShape = getrefshape(ip)
             ipg = Lagrange{RefShape, 1}()
 
@@ -587,6 +586,191 @@ end
             test_interpolation_functionals.(Hcurl_interpolations)
             test_interpolation_functionals.(Hdiv_interpolations)
         end
+
+        # Test boundary conditions (ProjectedDirichlet)
+        # Depending on the interpolation, we have different polynomials orders on the facets
+        _facet_poly_order(ip::Nedelec) = Ferrite.getorder(ip) - 1
+        _facet_poly_order(ip::RaviartThomas) = Ferrite.getorder(ip) - 1
+        _facet_poly_order(ip::BrezziDouglasMarini) = Ferrite.getorder(ip)
+        # Based on this order, p_facet, we expect that we should fullfill different criteria.
+        # * If we prescribe a polynomial function to ProjectedDirichlet with a lower or equal order
+        #   than p_facet, we expect that the interpolation should match the provided function pointwise.
+        # * If we prescribe a polynomial function with higher order, but lower order than what the quadrature
+        #   rule in ProjectedDirichlet can integrate exactly, we expect that the integral over the boundary are equal.
+        # * If we prescribe a polynomial one order lower than we can integrate exactly, and p_facet ≥ 1,
+        #   we expect that the integrated linear moment equation, ∫ x f(x) dx, is integrated exactly
+        # The following tests check those properties for the H(div) and H(curl) interpolations.
+
+        cell_type(::Type{RefLine}) = Line
+        cell_type(::Type{RefTriangle}) = Triangle
+        cell_type(::Type{RefQuadrilateral}) = Quadrilateral
+        cell_type(::Type{RefTetrahedron}) = Tetrahedron
+        cell_type(::Type{RefHexahedron}) = Hexahedron
+        function _setup_dh_fv_for_bc_test(ip::Interpolation; nel = 3, qr_order = 6)
+            RefShape = Ferrite.getrefshape(ip)
+            CT = cell_type(RefShape)
+            dim = Ferrite.getrefdim(CT) # dim=sdim=vdim
+            grid = generate_grid(CT, ntuple(Returns(nel), dim), -0.25 * ones(Vec{dim}), 0.2 * ones(Vec{dim}))
+            transform_coordinates!(grid, x -> x + 0.25 * Vec(ntuple(i -> abs(x[dim - i + 1])^(1 + i / dim), dim)))
+            qr = FacetQuadratureRule{RefShape}(qr_order)
+            fv = FacetValues(qr, ip, geometric_interpolation(CT))
+            dh = close!(add!(DofHandler(grid), :u, ip))
+            return dh, (fv,)
+        end
+        function _setup_dh_fv_for_bc_test(ips::Tuple{Interpolation{<:RefTriangle}, Interpolation{<:RefQuadrilateral}}; nel = 3, qr_order = 6)
+            dim = 2
+            trigrid = generate_grid(Triangle, (3, 3), -0.25 * ones(Vec{dim}), 0.2 * ones(Vec{dim}))
+            #trigrid = generate_grid(Triangle, (3, 3))
+            mixgrid, nr_quad = grid_with_inserted_quad(trigrid, (3, 4); update_sets = false) # Quad on bottom facet.
+            empty!(mixgrid.facetsets)
+            addfacetset!(mixgrid, "bottom", x -> x[2] ≈ -0.25)
+            transform_coordinates!(mixgrid, x -> x + 0.25 * Vec(ntuple(i -> abs(x[dim - i + 1])^(1 + i / dim), dim)))
+            qr_tri = FacetQuadratureRule{RefTriangle}(qr_order)
+            fv_tri = FacetValues(qr_tri, ips[1], geometric_interpolation(Triangle))
+            qr_quad = FacetQuadratureRule{RefQuadrilateral}(qr_order)
+            fv_quad = FacetValues(qr_quad, ips[2], geometric_interpolation(Quadrilateral))
+            dh = DofHandler(mixgrid)
+            sdh_tri = SubDofHandler(dh, setdiff(1:getncells(mixgrid), Set(nr_quad)))
+            add!(sdh_tri, :u, ips[1])
+            sdh_quad = SubDofHandler(dh, Set(nr_quad))
+            add!(sdh_quad, :u, ips[2])
+            close!(dh)
+            return dh, (fv_tri, fv_quad)
+        end
+
+        function test_bc_integral(
+                f_bc::Function, check_fun::Function, moment_fun::Function, dh::DofHandler, facetset, fvs::Tuple;
+                tol = 1.0e-6, custom_qr_order = nothing, pointwise_check, kwargs...
+            )
+            grid = Ferrite.get_grid(dh)
+            dbc = if custom_qr_order === nothing
+                ProjectedDirichlet(:u, facetset, f_bc)
+            else
+                ProjectedDirichlet(:u, facetset, f_bc; qr_order = custom_qr_order)
+            end
+            ch = close!(add!(ConstraintHandler(dh), dbc))
+            a = zeros(ndofs(dh))
+            apply!(a, ch)
+            fv1 = first(fvs)
+            test_val = zero(f_bc(get_node_coordinate(grid, 1), 0.0, getnormal(fv1, 1)))
+            check_val = zero(check_fun(zero(Ferrite.shape_value_type(fv1)), getnormal(fv1, 1)))
+            @assert typeof(test_val) === typeof(check_val)
+            for (sdh, fv) in zip(dh.subdofhandlers, fvs)
+                tv, cv = test_bc_integral(f_bc, check_fun, moment_fun, sdh, a, facetset, fv; pointwise_check, kwargs...)
+                test_val += tv
+                check_val += cv
+            end
+            @test norm(test_val - check_val) < tol
+        end
+
+        function test_bc_integral(
+                f_bc::Function, check_fun::Function, moment_fun::Function, sdh::SubDofHandler, a, facetset, fv::FacetValues;
+                pointwise_check, patchwise_check = true
+            )
+            grid = sdh.dh.grid
+            test_val = zero(f_bc(get_node_coordinate(grid, 1), 0.0, getnormal(fv, 1)))
+            check_val = zero(check_fun(zero(Ferrite.shape_value_type(fv)), getnormal(fv, 1)))
+            @assert typeof(test_val) === typeof(check_val)
+            for (cellidx, facetidx) in facetset
+                cellidx ∈ sdh.cellset || continue
+                cell_coords = getcoordinates(grid, cellidx)
+                reinit!(fv, getcells(grid, cellidx), cell_coords, facetidx)
+                ae = a[celldofs(sdh, cellidx)]
+                for q_point in 1:getnquadpoints(fv)
+                    dΓ = getdetJdV(fv, q_point)
+                    u = function_value(fv, q_point, ae)
+                    n = getnormal(fv, q_point)
+                    x = spatial_coordinate(fv, q_point, cell_coords)
+                    check_fun_val = check_fun(u, n)
+                    bc_fun_val = f_bc(x, 0.0, n)
+                    check_val += moment_fun(x) * check_fun_val * dΓ
+                    test_val += moment_fun(x) * bc_fun_val * dΓ
+                    if pointwise_check
+                        @test check_fun_val ≈ bc_fun_val
+                    end
+                end
+                if patchwise_check
+                    @test check_val ≈ test_val
+                end
+            end
+            return test_val, check_val
+        end
+
+        @testset "H(div) BC" begin
+            for ip in Hdiv_interpolations
+                @testset "$ip" begin
+                    dh, fv = _setup_dh_fv_for_bc_test(ip)
+                    linear_x1(x, _, _) = x[1]
+                    quadratic_x1(x, _, _) = (x[1] - 0.3)^2
+                    nonlinear(x, _, _) = 100 * (x[1] - 0.3)^4
+                    funs = [
+                        (Returns(0.0), true),
+                        (Returns(rand()), true),
+                        (linear_x1, _facet_poly_order(ip) ≥ 1),
+                        (quadratic_x1, _facet_poly_order(ip) ≥ 2),
+                        (nonlinear, false), # Only test integral quantities using high-order integration.
+                    ]
+                    for (f_bc, pointwise_check) in funs
+                        @testset "f_bc = $f_bc" begin
+                            for facetset in values(dh.grid.facetsets)
+                                custom_qr_order = f_bc === nonlinear ? 5 : nothing
+                                test_bc_integral(f_bc, ⋅, Returns(1), dh, facetset, fv; pointwise_check, custom_qr_order)
+                                if _facet_poly_order(ip) ≥ 1 && f_bc !== nonlinear # Linear facet polynomial should pass moment test.
+                                    test_bc_integral(f_bc, ⋅, x -> x[1], dh, facetset, fv; pointwise_check = false)
+                                end
+                            end
+                        end
+                    end
+                end
+            end
+        end
+
+        @testset "H(curl) BC" begin
+            ips = Any[Hcurl_interpolations...]
+            push!(ips, (Nedelec{RefTriangle, 1}(), Nedelec{RefQuadrilateral, 1}())) # Mixed case
+            for interp in ips
+                ip = isa(interp, Tuple) ? interp[1] : interp
+                dim = Ferrite.getrefdim(getrefshape(ip))
+                dim == 3 && continue # TODO: Not yet implemented
+                @testset "$interp" begin
+                    dh, fvs = _setup_dh_fv_for_bc_test(interp)
+                    v3 = rand()
+                    linear_x1(x, _, _) = x[1] * Vec((0.0, 0.0, v3))
+                    quadratic_x1(x, _, _) = (x[1] - 0.3)^2 * Vec((0.0, 0.0, v3))
+                    nonlinear(x, _, _) = Vec((0.0, 0.0, v3)) * (100 * (x[1] + 0.3)^4)
+                    funs = [
+                        (Returns(zero(Vec{3})), true),
+                        (Returns(Vec((0.0, 0.0, rand()))), true),
+                        (linear_x1, _facet_poly_order(ip) ≥ 1),
+                        (quadratic_x1, _facet_poly_order(ip) ≥ 2),
+                        (nonlinear, false),
+                    ]
+                    for (f_bc, pointwise_check) in funs
+                        @testset "f_bc = $f_bc" begin
+                            for facetset in values(dh.grid.facetsets)
+                                custom_qr_order = f_bc === nonlinear ? 5 : nothing
+                                test_bc_integral(f_bc, ×, Returns(1.0), dh, facetset, fvs; pointwise_check, custom_qr_order)
+                                if _facet_poly_order(ip) ≥ 1  && f_bc !== nonlinear
+                                    test_bc_integral(f_bc, ×, x -> x[1], dh, facetset, fvs; pointwise_check = false)
+                                end
+                            end
+                        end
+                    end
+                end
+            end
+        end
+
+        @testset "ProjectedDirichlet error path" begin
+            dh_H1, _ = _setup_dh_fv_for_bc_test(Lagrange{RefTriangle, 1}()^2; nel = 1, qr_order = 1)
+            dh_L2, _ = _setup_dh_fv_for_bc_test(DiscontinuousLagrange{RefTriangle, 1}()^2; nel = 1, qr_order = 1)
+            dh_Hcurl_3d, _ = _setup_dh_fv_for_bc_test(Nedelec{RefTetrahedron, 1}(); nel = 1, qr_order = 1)
+            for dh in (dh_H1, dh_L2, dh_Hcurl_3d)
+                dbc = ProjectedDirichlet(:u, Set([FacetIndex(1, 1)]), Returns(zero(Vec{2})))
+                ch = add!(ConstraintHandler(dh), dbc)
+                @test_throws "ProjectedDirichlet is not implemented for" close!(ch)
+            end
+        end
+
     end
 
 end # testset


Reviews:
user: login: fredrikekre
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-14T21:16:37Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-14T21:31:00Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-14T21:43:02Z
user: login: fredrikekre
body: I haven't gone through the math but if it is sound and tests cover it then :+1: 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-02-17T12:21:24Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-17T18:27:56Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-17T18:31:02Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-17T18:32:04Z
user: login: fredrikekre
body: Is it too magical if `Dirichlet(::HcurlInterpolation)` adds a `WeakDirichlet`? Probably, but would be kind of nice to not have to change the BC when changing interpolations.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-02-18T13:57:11Z
user: login: termi-official
body: Really cool work here, thanks!

I was asking myself if this infrastructure can help us with applying normal and tangential boundary conditions (e.g. static sliding contact)?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T12:26:42Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:05:53Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:08:25Z
user: login: termi-official
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:11:38Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:15:27Z
user: login: fredrikekre
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:18:40Z
user: login: termi-official
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:20:42Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:22:12Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:29:58Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:33:26Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:36:07Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:46:49Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:59:54Z
user: login: termi-official
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T16:03:19Z
user: login: termi-official
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T16:06:20Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T19:58:25Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T20:13:51Z
user: login: termi-official
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T20:43:22Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T10:41:06Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T10:42:33Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T10:54:52Z
user: login: termi-official
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T11:02:12Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T12:16:36Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T13:37:03Z
user: login: fredrikekre
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T04:18:15Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T05:08:04Z
user: login: termi-official
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T10:22:31Z
user: login: KnutAM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T14:57:00Z
user: login: termi-official
body: LGTM

> And for hcurl: https://www.dealii.org/current/doxygen/deal.II/group__constraints.html#ga190b302dc1ee7c89727c94c083d132d2

This is quite a smart strategy here. However, we need to implement EdgeValues and the related topological queries to reproduce this efficiently.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-28T15:26:18Z

Review comments:
diff_hunk: @@ -450,6 +450,7 @@ struct DiscontinuousLagrange{shape, order} <: ScalarInterpolation{shape, order}
         return new{shape, order}()
     end
 end
+function_space(::DiscontinuousLagrange) = Val(:L2)
path: src/interpolations.jl
user: login: fredrikekre
body: Cleaner to have proper types?
created_at: 2025-02-14T21:16:36Z
author_association: MEMBER
diff_hunk: @@ -450,6 +450,7 @@ struct DiscontinuousLagrange{shape, order} <: ScalarInterpolation{shape, order}
         return new{shape, order}()
     end
 end
+function_space(::DiscontinuousLagrange) = Val(:L2)
path: src/interpolations.jl
user: login: KnutAM
body: Agreed! But maybe, 
```julia
struct FunctionSpace{space}
    FunctionSpace(space::Symbol) = new{space}()
end
```
is enough, instead of `L2FunctionSpace`, `H1FunctionSpace`, etc...? 
created_at: 2025-02-14T21:31:00Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1821,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::Dirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, dbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in dbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.idbcs, dbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    n = maximum(length, dirichlet_facetdof_indices(ip))
+    Kᶠ = zeros(n, n)
+    aᶠ = zeros(n)
+    fᶠ = zeros(n)
+    for fc in FacetIterator(dh, facets)
+        reinit!(fv, fc)
+        shape_nrs = dirichlet_facetdof_indices(ip)[getcurrentfacet(fv)]
+        solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, f, fv, shape_nrs, getcoordinates(fc), time)
+        for (idof, shape_nr) in enumerate(shape_nrs)
+            globaldof = celldofs(fc)[shape_nr]
+            dbc_index = dofmapping[globaldof]
+            # Only DBC dofs are currently update!-able so don't modify inhomogeneities
+            # for affine constraints
+            if dofcoefficients[dbc_index] === nothing
+                inhomogeneities[dbc_index] = aᶠ[idof]
+            end
+        end
+    end
+    return nothing
+end
+
+function solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    fill!(Kᶠ, 0)
+    fill!(fᶠ, 0)
+    # Support varying number of facetdofs (for ref shapes with different facet types)
+    for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
+        Kᶠ[i, i] = 1
+    end
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: Currently, we don't have any H(div) or H(curl) interpolations with different number of shape functions associated with different facets. But theoretically, we could, hence this addition which is not tested. 

Removing it will case a singular matrix if such are implemented in the future, alternatively we could check and throw if `size(K,1) != length(shape_nrs)`. 
created_at: 2025-02-14T21:43:02Z
author_association: MEMBER
diff_hunk: @@ -83,6 +135,7 @@ A collection of constraints associated with the dof handler `dh`.
 """
 mutable struct ConstraintHandler{DH <: AbstractDofHandler, T}
     const dbcs::Vector{Dirichlet}
+    const idbcs::Vector{WeakDirichlet}
path: src/Dofs/ConstraintHandler.jl
user: login: fredrikekre
body: Whats the `i` for? Perhaps:
```suggestion
    const wdbcs::Vector{WeakDirichlet}
```
created_at: 2025-02-17T12:11:50Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,58 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets, f::Function; qr_order = -1)
+
+An `WeakDirichlet` conditions enforces conditions for `field_name` on the boundary for
path: src/Dofs/ConstraintHandler.jl
user: login: fredrikekre
body: ```suggestion
A `WeakDirichlet` condition enforces conditions for `field_name` on the boundary for
```
created_at: 2025-02-17T12:12:13Z
author_association: MEMBER
diff_hunk: @@ -410,12 +463,18 @@ function update!(ch::ConstraintHandler, time::Real = 0.0)
         # g(x, t) = f(x) that discards the second parameter so that _update! can always call
         # the function with two arguments internally.
         wrapper_f = hasmethod(dbc.f, Tuple{get_coordinate_type(get_grid(ch.dh)), typeof(time)}) ? dbc.f : (x, _) -> dbc.f(x)
+        #TODO: Temporary way to get the ip, not safe as it just get the first sdh with field_name in it.
+        field_idx = find_field(ch.dh, dbc.field_name)
+        ip = getfieldinterpolation(ch.dh, field_idx)
         # Function barrier
         _update!(
-            ch.inhomogeneities, wrapper_f, dbc.facets, dbc.field_name, dbc.local_facet_dofs, dbc.local_facet_dofs_offset,
+            ch.inhomogeneities, wrapper_f, dbc.facets, dbc.local_facet_dofs, dbc.local_facet_dofs_offset,
             dbc.components, ch.dh, ch.bcvalues[i], ch.dofmapping, ch.dofcoefficients, time
         )
     end
+    for (i, dbc) in pairs(ch.idbcs)
path: src/Dofs/ConstraintHandler.jl
user: login: fredrikekre
body: Index `i` unused.
created_at: 2025-02-17T12:14:31Z
author_association: MEMBER
diff_hunk: @@ -410,12 +463,18 @@ function update!(ch::ConstraintHandler, time::Real = 0.0)
         # g(x, t) = f(x) that discards the second parameter so that _update! can always call
         # the function with two arguments internally.
         wrapper_f = hasmethod(dbc.f, Tuple{get_coordinate_type(get_grid(ch.dh)), typeof(time)}) ? dbc.f : (x, _) -> dbc.f(x)
+        #TODO: Temporary way to get the ip, not safe as it just get the first sdh with field_name in it.
+        field_idx = find_field(ch.dh, dbc.field_name)
+        ip = getfieldinterpolation(ch.dh, field_idx)
         # Function barrier
         _update!(
-            ch.inhomogeneities, wrapper_f, dbc.facets, dbc.field_name, dbc.local_facet_dofs, dbc.local_facet_dofs_offset,
+            ch.inhomogeneities, wrapper_f, dbc.facets, dbc.local_facet_dofs, dbc.local_facet_dofs_offset,
path: src/Dofs/ConstraintHandler.jl
user: login: fredrikekre
body: Unrelated change? No need to undo this but just curious.
created_at: 2025-02-17T12:15:16Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1821,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::Dirichlet and fields in the ConstraintHandler's DofHandler")
path: src/Dofs/ConstraintHandler.jl
user: login: fredrikekre
body: ```suggestion
    dbc_added || error("No overlap between dbc::WeakDirichlet and fields in the ConstraintHandler's DofHandler")
```
created_at: 2025-02-17T12:16:28Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1821,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::Dirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, dbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in dbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.idbcs, dbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    n = maximum(length, dirichlet_facetdof_indices(ip))
path: src/Dofs/ConstraintHandler.jl
user: login: fredrikekre
body: Is `n` the number of dofs related to a single facet? So for low orders `n = 1` and we solve a scalar eq?
created_at: 2025-02-17T12:19:45Z
author_association: MEMBER
diff_hunk: @@ -83,6 +135,7 @@ A collection of constraints associated with the dof handler `dh`.
 """
 mutable struct ConstraintHandler{DH <: AbstractDofHandler, T}
     const dbcs::Vector{Dirichlet}
+    const idbcs::Vector{WeakDirichlet}
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: Yes, absolutely (I think I had another name before, thanks for the detailed review and spotting of these things!)
created_at: 2025-02-17T18:27:56Z
author_association: MEMBER
diff_hunk: @@ -410,12 +463,18 @@ function update!(ch::ConstraintHandler, time::Real = 0.0)
         # g(x, t) = f(x) that discards the second parameter so that _update! can always call
         # the function with two arguments internally.
         wrapper_f = hasmethod(dbc.f, Tuple{get_coordinate_type(get_grid(ch.dh)), typeof(time)}) ? dbc.f : (x, _) -> dbc.f(x)
+        #TODO: Temporary way to get the ip, not safe as it just get the first sdh with field_name in it.
+        field_idx = find_field(ch.dh, dbc.field_name)
+        ip = getfieldinterpolation(ch.dh, field_idx)
         # Function barrier
         _update!(
-            ch.inhomogeneities, wrapper_f, dbc.facets, dbc.field_name, dbc.local_facet_dofs, dbc.local_facet_dofs_offset,
+            ch.inhomogeneities, wrapper_f, dbc.facets, dbc.local_facet_dofs, dbc.local_facet_dofs_offset,
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: Yes, I think the reason was that I tried to use the same function, but then decided it was cleaner to use `_update_weak_dbc!` instead. So this was a left-over, sorry about that, but I guess it makes sense to remove the argument, so leaving it in then. 
created_at: 2025-02-17T18:31:02Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1821,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::Dirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, dbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in dbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.idbcs, dbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    n = maximum(length, dirichlet_facetdof_indices(ip))
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: Yes. 
In general, we could do a lot of performance optimization here if required, but when I did some benchmark I didn't see that as a bottleneck in the Maxwell tutorial. 
created_at: 2025-02-17T18:32:04Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1821,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::Dirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, dbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in dbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.idbcs, dbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    n = maximum(length, dirichlet_facetdof_indices(ip))
+    Kᶠ = zeros(n, n)
+    aᶠ = zeros(n)
+    fᶠ = zeros(n)
+    for fc in FacetIterator(dh, facets)
+        reinit!(fv, fc)
+        shape_nrs = dirichlet_facetdof_indices(ip)[getcurrentfacet(fv)]
+        solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, f, fv, shape_nrs, getcoordinates(fc), time)
+        for (idof, shape_nr) in enumerate(shape_nrs)
+            globaldof = celldofs(fc)[shape_nr]
+            dbc_index = dofmapping[globaldof]
+            # Only DBC dofs are currently update!-able so don't modify inhomogeneities
+            # for affine constraints
+            if dofcoefficients[dbc_index] === nothing
+                inhomogeneities[dbc_index] = aᶠ[idof]
+            end
+        end
+    end
+    return nothing
+end
+
+function solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    fill!(Kᶠ, 0)
+    fill!(fᶠ, 0)
+    # Support varying number of facetdofs (for ref shapes with different facet types)
+    for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
+        Kᶠ[i, i] = 1
+    end
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: We also have the very same issue with H1/L2 interpolations and this property, so let us not introduce this assumption for now in parts of the code.
created_at: 2025-04-14T12:08:13Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1841,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::WeakDirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, wdbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in wdbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.wdbcs, wdbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    max_dofs_per_facet = maximum(length, dirichlet_facetdof_indices(ip))
+    Kᶠ = zeros(max_dofs_per_facet, max_dofs_per_facet)
+    aᶠ = zeros(max_dofs_per_facet)
+    fᶠ = zeros(max_dofs_per_facet)
+    for fc in FacetIterator(dh, facets)
+        reinit!(fv, fc)
+        shape_nrs = dirichlet_facetdof_indices(ip)[getcurrentfacet(fv)]
+        solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, f, fv, shape_nrs, getcoordinates(fc), time)
+        for (idof, shape_nr) in enumerate(shape_nrs)
+            globaldof = celldofs(fc)[shape_nr]
+            dbc_index = dofmapping[globaldof]
+            # Only DBC dofs are currently update!-able so don't modify inhomogeneities
+            # for affine constraints
+            if dofcoefficients[dbc_index] === nothing
+                inhomogeneities[dbc_index] = aᶠ[idof]
+            end
+        end
+    end
+    return nothing
+end
+
+function solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    fill!(Kᶠ, 0)
+    fill!(fᶠ, 0)
+    # Support varying number of facetdofs (for ref shapes with different facet types)
+    for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
+        Kᶠ[i, i] = 1
+    end
+    integrate_weak_dbc!(Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    aᶠ .= Kᶠ \ fᶠ # Could be done non-allocating if required, using e.g. SMatrix
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: Should we cache the factorization of K for time-dependent problems (e.g. transient Maxwell)?
created_at: 2025-04-14T12:09:15Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: Can we discuss the name for a moment? I think `WeakDirichlet` is misleading, because at least from the literature I know ,,weak bcs'' are usually introduced by adding energy terms (or weak form terms if we have no energy) to force the solution towards the right boundary condition, as done e.g. with Nitzsche method. However, the presented method here strongly enforces the boundary condition.

Can we merge this infrastructure into the `Dirichlet` struct directly and control the application by dispatching on the new conformity trait?

I also noticed that we are missing some docstring for the application of DBCs in L2 elements. :D 
created_at: 2025-04-14T12:13:05Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1841,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::WeakDirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, wdbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in wdbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.wdbcs, wdbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    max_dofs_per_facet = maximum(length, dirichlet_facetdof_indices(ip))
+    Kᶠ = zeros(max_dofs_per_facet, max_dofs_per_facet)
+    aᶠ = zeros(max_dofs_per_facet)
+    fᶠ = zeros(max_dofs_per_facet)
+    for fc in FacetIterator(dh, facets)
+        reinit!(fv, fc)
+        shape_nrs = dirichlet_facetdof_indices(ip)[getcurrentfacet(fv)]
+        solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, f, fv, shape_nrs, getcoordinates(fc), time)
+        for (idof, shape_nr) in enumerate(shape_nrs)
+            globaldof = celldofs(fc)[shape_nr]
+            dbc_index = dofmapping[globaldof]
+            # Only DBC dofs are currently update!-able so don't modify inhomogeneities
+            # for affine constraints
+            if dofcoefficients[dbc_index] === nothing
+                inhomogeneities[dbc_index] = aᶠ[idof]
+            end
+        end
+    end
+    return nothing
+end
+
+function solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    fill!(Kᶠ, 0)
+    fill!(fᶠ, 0)
+    # Support varying number of facetdofs (for ref shapes with different facet types)
+    for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
+        Kᶠ[i, i] = 1
+    end
+    integrate_weak_dbc!(Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    aᶠ .= Kᶠ \ fᶠ # Could be done non-allocating if required, using e.g. SMatrix
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: Also, do we know f there is any relation between unisolvence and the invertibility of K?
created_at: 2025-04-14T12:14:03Z
author_association: MEMBER
diff_hunk: @@ -51,6 +51,11 @@ const InterpolationByDim{dim} = Interpolation{<:AbstractRefShape{dim}}
 abstract type ScalarInterpolation{refshape, order} <: Interpolation{refshape, order} end
 abstract type VectorInterpolation{vdim, refshape, order} <: Interpolation{refshape, order} end
 
+# Conformity, :H1, :Hdiv, :Hcurl, and :L2 currently implemented.
+struct Conformity{K}
+    Conformity(conformity::Symbol) = new{conformity}()
+end
path: src/interpolations.jl
user: login: termi-official
body: Out of curiosity: Is there a reason why we do this instead of four different structs?
created_at: 2025-04-14T12:17:46Z
author_association: MEMBER
diff_hunk: @@ -410,12 +483,18 @@ function update!(ch::ConstraintHandler, time::Real = 0.0)
         # g(x, t) = f(x) that discards the second parameter so that _update! can always call
         # the function with two arguments internally.
         wrapper_f = hasmethod(dbc.f, Tuple{get_coordinate_type(get_grid(ch.dh)), typeof(time)}) ? dbc.f : (x, _) -> dbc.f(x)
+        #TODO: Temporary way to get the ip, not safe as it just get the first sdh with field_name in it.
+        field_idx = find_field(ch.dh, dbc.field_name)
+        ip = getfieldinterpolation(ch.dh, field_idx)
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: Unused code?
created_at: 2025-04-14T12:24:00Z
author_association: MEMBER
diff_hunk: @@ -51,6 +51,11 @@ const InterpolationByDim{dim} = Interpolation{<:AbstractRefShape{dim}}
 abstract type ScalarInterpolation{refshape, order} <: Interpolation{refshape, order} end
 abstract type VectorInterpolation{vdim, refshape, order} <: Interpolation{refshape, order} end
 
+# Conformity, :H1, :Hdiv, :Hcurl, and :L2 currently implemented.
+struct Conformity{K}
+    Conformity(conformity::Symbol) = new{conformity}()
+end
path: src/interpolations.jl
user: login: KnutAM
body: Seemed unnecessary to introduce the hierarchy below an `AbstractConformity` with several subtypes, when e.g. `Conformity{:H1}` reads quite good for the dispatch.

But could be changed too of course if that is considered better. 
created_at: 2025-04-14T14:05:53Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: > Can we merge this infrastructure into the Dirichlet struct directly and control the application by dispatching on the new conformity trait?

I think it would be wiser not to do that (ref: https://github.com/Ferrite-FEM/Ferrite.jl/pull/1151#issuecomment-2665862101 above). But of course, we could consider changing `Dirichlet` to be a wrapper/input type, and then store separate structs internally depending on how we want to apply BCs. 
created_at: 2025-04-14T14:08:25Z
author_association: MEMBER
diff_hunk: @@ -51,6 +51,11 @@ const InterpolationByDim{dim} = Interpolation{<:AbstractRefShape{dim}}
 abstract type ScalarInterpolation{refshape, order} <: Interpolation{refshape, order} end
 abstract type VectorInterpolation{vdim, refshape, order} <: Interpolation{refshape, order} end
 
+# Conformity, :H1, :Hdiv, :Hcurl, and :L2 currently implemented.
+struct Conformity{K}
+    Conformity(conformity::Symbol) = new{conformity}()
+end
path: src/interpolations.jl
user: login: termi-official
body: I was not even thinking about a type hierarchy here and was just curious. For me having the types directly available looks a bit more stable for users since the type accepts typos, but for the short names here it should be okay and these should be easy to find. @fredrikekre do you see any obvious fallacies with this pattern?
created_at: 2025-04-14T14:11:38Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: > However, the presented method here strongly enforces the boundary condition.

You are right, ref [Bazilevs and Hughes (2007) Weak imposition of Dirichlet boundary conditions in fluid mechanics](https://www.sciencedirect.com/science/article/pii/S0045793005001258)

However, if the method here is used for an H1 interpolation, it would give a different result compared to the current Dirichlet which satisfies the prescribed value point-wise, whereas the present one only in a weak/integral sense.

How about going back to the original name - "IntegrableDirichlet"? 
created_at: 2025-04-14T14:15:26Z
author_association: MEMBER
diff_hunk: @@ -51,6 +51,11 @@ const InterpolationByDim{dim} = Interpolation{<:AbstractRefShape{dim}}
 abstract type ScalarInterpolation{refshape, order} <: Interpolation{refshape, order} end
 abstract type VectorInterpolation{vdim, refshape, order} <: Interpolation{refshape, order} end
 
+# Conformity, :H1, :Hdiv, :Hcurl, and :L2 currently implemented.
+struct Conformity{K}
+    Conformity(conformity::Symbol) = new{conformity}()
+end
path: src/interpolations.jl
user: login: fredrikekre
body: Doesn't really matter since there will be four different concrete structs either way.
created_at: 2025-04-14T14:18:40Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: > However, if the method here is used for an H1 interpolation, it would give a different result compared to the current Dirichlet which satisfies the prescribed value point-wise, whereas the present one only in a weak/integral sense.

Oh, i see, it "smears out" the Boundary condition because we use Gauss-Legendre quadrature by default, right? The methods should coincide when using Gauss-Lobatto quadrature tho.
created_at: 2025-04-14T14:20:42Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1841,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::WeakDirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, wdbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in wdbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.wdbcs, wdbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    max_dofs_per_facet = maximum(length, dirichlet_facetdof_indices(ip))
+    Kᶠ = zeros(max_dofs_per_facet, max_dofs_per_facet)
+    aᶠ = zeros(max_dofs_per_facet)
+    fᶠ = zeros(max_dofs_per_facet)
+    for fc in FacetIterator(dh, facets)
+        reinit!(fv, fc)
+        shape_nrs = dirichlet_facetdof_indices(ip)[getcurrentfacet(fv)]
+        solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, f, fv, shape_nrs, getcoordinates(fc), time)
+        for (idof, shape_nr) in enumerate(shape_nrs)
+            globaldof = celldofs(fc)[shape_nr]
+            dbc_index = dofmapping[globaldof]
+            # Only DBC dofs are currently update!-able so don't modify inhomogeneities
+            # for affine constraints
+            if dofcoefficients[dbc_index] === nothing
+                inhomogeneities[dbc_index] = aᶠ[idof]
+            end
+        end
+    end
+    return nothing
+end
+
+function solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    fill!(Kᶠ, 0)
+    fill!(fᶠ, 0)
+    # Support varying number of facetdofs (for ref shapes with different facet types)
+    for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
+        Kᶠ[i, i] = 1
+    end
+    integrate_weak_dbc!(Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    aᶠ .= Kᶠ \ fᶠ # Could be done non-allocating if required, using e.g. SMatrix
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: > Should we cache the factorization of K for time-dependent problems (e.g. transient Maxwell)?

I think the better optimization would be to use StaticArrays, but so far I didn't see a need to optimize this. 
created_at: 2025-04-14T14:22:11Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1841,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::WeakDirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, wdbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in wdbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.wdbcs, wdbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    max_dofs_per_facet = maximum(length, dirichlet_facetdof_indices(ip))
+    Kᶠ = zeros(max_dofs_per_facet, max_dofs_per_facet)
+    aᶠ = zeros(max_dofs_per_facet)
+    fᶠ = zeros(max_dofs_per_facet)
+    for fc in FacetIterator(dh, facets)
+        reinit!(fv, fc)
+        shape_nrs = dirichlet_facetdof_indices(ip)[getcurrentfacet(fv)]
+        solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, f, fv, shape_nrs, getcoordinates(fc), time)
+        for (idof, shape_nr) in enumerate(shape_nrs)
+            globaldof = celldofs(fc)[shape_nr]
+            dbc_index = dofmapping[globaldof]
+            # Only DBC dofs are currently update!-able so don't modify inhomogeneities
+            # for affine constraints
+            if dofcoefficients[dbc_index] === nothing
+                inhomogeneities[dbc_index] = aᶠ[idof]
+            end
+        end
+    end
+    return nothing
+end
+
+function solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    fill!(Kᶠ, 0)
+    fill!(fᶠ, 0)
+    # Support varying number of facetdofs (for ref shapes with different facet types)
+    for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
+        Kᶠ[i, i] = 1
+    end
+    integrate_weak_dbc!(Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    aᶠ .= Kᶠ \ fᶠ # Could be done non-allocating if required, using e.g. SMatrix
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: > Also, do we know f there is any relation between unisolvence and the invertibility of K?

From Slack discussion:
If the normal/tangential values on the boundaries are linearly independent, K will be invertible (of course assuming sufficiently high integration order). 
If one would introduce a modified BDM interpolation, changing
BDM element could be modified on edge 2 from
```julia
i == 3 && return Vec(-2x - 6y + 2, 4y) # N ⋅ n = (6y - 2)
i == 4 && return Vec(4x + 6y - 4, -2y) # N ⋅ n = (4 - 6y)
```
to
```julia
i == 3 && return Vec(-2x - 6y + 2, 4y) # N ⋅ n = (6y - 2)
i == 4 && return Vec(-2x^2 - 6y + 2, 4y) # N ⋅ n = (6y - 2)
```
such that i=4 should still be linearly independent form all other shape functions, but on edge 2 (x=0), 3 and 4 are the same.

But with this modification, it should fail the reference moment test.
Therefore, as long as an interpolation passes the reference moment tests, it should result in an invertible K.
created_at: 2025-04-14T14:29:58Z
author_association: MEMBER
diff_hunk: @@ -1762,3 +1841,146 @@ function _condense_local!(
     end
     return
 end
+
+
+function _default_bc_qr_order(user_provided::Int, ip::Interpolation)
+    user_provided > 0 && return user_provided
+    return _default_bc_qr_order(ip)
+end
+# Q&D default, should be more elaborated
+_default_bc_qr_order(::Interpolation{<:Any, order}) where {order} = 2 * order
+
+function add!(ch::ConstraintHandler, dbc::WeakDirichlet)
+    # Duplicate the Dirichlet constraint for every SubDofHandler
+    dbc_added = false
+    for sdh in ch.dh.subdofhandlers
+        # Skip if the constrained field does not live on this sub domain
+        dbc.field_name in sdh.field_names || continue
+        # Compute the intersection between dbc.set and the cellset of this
+        # SubDofHandler and skip if the set is empty
+        filtered_set = filter_dbc_set(get_grid(ch.dh), sdh.cellset, dbc.facets)
+        isempty(filtered_set) && continue
+        # Fetch information about the field on this SubDofHandler
+        field_idx = find_field(sdh, dbc.field_name)
+        interpolation = getfieldinterpolation(sdh, field_idx)
+        CT = getcelltype(sdh) # Same celltype enforced in SubDofHandler constructor
+        qr_order = _default_bc_qr_order(dbc.qr_order, interpolation)
+        fqr = FacetQuadratureRule{getrefshape(interpolation)}(qr_order)
+        fv = FacetValues(fqr, interpolation, geometric_interpolation(CT))
+        local_facet_dofs, local_facet_dofs_offset =
+            _local_facet_dofs_for_bc(get_base_interpolation(interpolation), 1, 1, field_offset(sdh, field_idx), dirichlet_facetdof_indices)
+        facet_dofs = ArrayOfVectorViews(local_facet_dofs_offset, local_facet_dofs, LinearIndices(1:(length(local_facet_dofs_offset) - 1)))
+
+        filtered_dbc = WeakDirichlet(dbc.f, filtered_set, dbc.field_name, qr_order, fv, facet_dofs)
+
+        _add!(ch, filtered_dbc, facet_dofs)
+
+        dbc_added = true
+    end
+    dbc_added || error("No overlap between dbc::WeakDirichlet and fields in the ConstraintHandler's DofHandler")
+    return ch
+end
+
+function _add!(ch::ConstraintHandler, wdbc::WeakDirichlet, facet_dofs)
+    # loop over all the faces in the set and add the global dofs to `constrained_dofs`
+    constrained_dofs = Int[]
+    cc = CellCache(ch.dh, UpdateFlags(; nodes = false, coords = false, dofs = true))
+    for (cellidx, facetidx) in wdbc.facets
+        reinit!(cc, cellidx)
+        local_dofs = facet_dofs[facetidx]
+        for d in local_dofs
+            push!(constrained_dofs, cc.dofs[d])
+        end
+    end
+
+    # save it to the ConstraintHandler
+    push!(ch.wdbcs, wdbc)
+    for d in constrained_dofs
+        add_prescribed_dof!(ch, d, NaN, nothing)
+    end
+    return ch
+end
+
+
+function _update_weak_dbc!(
+        inhomogeneities::Vector{T}, f::Function, facets::AbstractVecOrSet{FacetIndex}, fv::FacetValues, facet_dofs::ArrayOfVectorViews,
+        dh::AbstractDofHandler, dofmapping::Dict{Int, Int}, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, time::Real
+    ) where {T}
+    ip = function_interpolation(fv)
+    max_dofs_per_facet = maximum(length, dirichlet_facetdof_indices(ip))
+    Kᶠ = zeros(max_dofs_per_facet, max_dofs_per_facet)
+    aᶠ = zeros(max_dofs_per_facet)
+    fᶠ = zeros(max_dofs_per_facet)
+    for fc in FacetIterator(dh, facets)
+        reinit!(fv, fc)
+        shape_nrs = dirichlet_facetdof_indices(ip)[getcurrentfacet(fv)]
+        solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, f, fv, shape_nrs, getcoordinates(fc), time)
+        for (idof, shape_nr) in enumerate(shape_nrs)
+            globaldof = celldofs(fc)[shape_nr]
+            dbc_index = dofmapping[globaldof]
+            # Only DBC dofs are currently update!-able so don't modify inhomogeneities
+            # for affine constraints
+            if dofcoefficients[dbc_index] === nothing
+                inhomogeneities[dbc_index] = aᶠ[idof]
+            end
+        end
+    end
+    return nothing
+end
+
+function solve_weak_dbc!(aᶠ, Kᶠ, fᶠ, bc_fun, fv, shape_nrs, cell_coords, time)
+    fill!(Kᶠ, 0)
+    fill!(fᶠ, 0)
+    # Support varying number of facetdofs (for ref shapes with different facet types)
+    for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
+        Kᶠ[i, i] = 1
+    end
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: ```suggestion
    # Supporting varying number of facetdofs (for ref shapes with different facet types) requires
    # for i in (length(shape_nrs) + 1):size(Kᶠ, 1)
    #     Kᶠ[i, i] = 1
    # end
    @assert length(shape_nrs) == size(Kᶠ, 1)
```
created_at: 2025-04-14T14:33:26Z
author_association: MEMBER
diff_hunk: @@ -410,12 +483,18 @@ function update!(ch::ConstraintHandler, time::Real = 0.0)
         # g(x, t) = f(x) that discards the second parameter so that _update! can always call
         # the function with two arguments internally.
         wrapper_f = hasmethod(dbc.f, Tuple{get_coordinate_type(get_grid(ch.dh)), typeof(time)}) ? dbc.f : (x, _) -> dbc.f(x)
+        #TODO: Temporary way to get the ip, not safe as it just get the first sdh with field_name in it.
+        field_idx = find_field(ch.dh, dbc.field_name)
+        ip = getfieldinterpolation(ch.dh, field_idx)
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: ```suggestion
```
created_at: 2025-04-14T14:36:07Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: Yes, assuming the Gauss-Lobatto has equally many points as the number of dofs, but with higher integration orders it doesn't matter what integration rule is used. I think of it like this: 

![image](https://github.com/user-attachments/assets/8d550d89-e33e-41a0-a04e-a6e00c74fa8c)

created_at: 2025-04-14T14:46:49Z
author_association: MEMBER
diff_hunk: @@ -410,12 +483,18 @@ function update!(ch::ConstraintHandler, time::Real = 0.0)
         # g(x, t) = f(x) that discards the second parameter so that _update! can always call
         # the function with two arguments internally.
         wrapper_f = hasmethod(dbc.f, Tuple{get_coordinate_type(get_grid(ch.dh)), typeof(time)}) ? dbc.f : (x, _) -> dbc.f(x)
+        #TODO: Temporary way to get the ip, not safe as it just get the first sdh with field_name in it.
+        field_idx = find_field(ch.dh, dbc.field_name)
+        ip = getfieldinterpolation(ch.dh, field_idx)
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: Good spot!
created_at: 2025-04-14T14:59:54Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: This is indeed quite interesting, I have not thought about non-matching functions here. So the new boundary condition enforcement should be closer to the actual function on our Dirichlet boundary, because the new method minimizes the distance between the analytical function and our function space, while the standard method is "just" an interpolation.
created_at: 2025-04-14T16:03:19Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: I think we should add this sketch to the docs as it very precisely explains the difference to users.
created_at: 2025-04-14T16:06:20Z
author_association: MEMBER
diff_hunk: @@ -51,6 +51,11 @@ const InterpolationByDim{dim} = Interpolation{<:AbstractRefShape{dim}}
 abstract type ScalarInterpolation{refshape, order} <: Interpolation{refshape, order} end
 abstract type VectorInterpolation{vdim, refshape, order} <: Interpolation{refshape, order} end
 
+# Conformity, :H1, :Hdiv, :Hcurl, and :L2 currently implemented.
+struct Conformity{K}
+    Conformity(conformity::Symbol) = new{conformity}()
+end
path: src/interpolations.jl
user: login: KnutAM
body: Decided to change, I agree that it looks more standard, and the possible typo is a reasonable concern. The common type was only used for tests. 
created_at: 2025-04-14T19:58:25Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    WeakDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: https://ferrite-fem.github.io/Ferrite.jl/previews/PR1151/topics/boundary_conditions/#L2ProjectedDirichlet
created_at: 2025-04-14T20:13:51Z
author_association: MEMBER
diff_hunk: @@ -93,6 +93,21 @@ end
     Most examples make use of Dirichlet boundary conditions, for example [Heat
     Equation](@ref tutorial-heat-equation).
 
+## L2ProjectedDirichlet
+Some interpolations don't have nodal support points:
+``H(\mathrm{curl})`` interpolations, e.g., `Nedelec`, are associated to edges
+and ``H(\mathrm{div})`` interpolations, e.g. `RaviartThomas`, are associated to facets.
+While normal `Dirichlet` boundary conditions assume the existence of such nodal support points,
+Ferrite provides the `L2ProjectedDirichlet`, which instead finds the degree of freedom values that
+minimizes the L2-distance between the prescribed function, ``f(\boldsymbol{x},t,\boldsymbol{n})``,
+and the finite element interpolation space. Although standard interpolations are not currently supported,
+the figure below illustrates well the difference between applying a standard `Dirichlet` condition and an
+`L2ProjectedDirichlet` condition when the prescribed function cannot be described by the chosen FE-interpolation.
+
+![L2ProjectedDirichlet illustration](https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/L2ProjectedDirichlet.svg)
path: docs/src/topics/boundary_conditions.md
user: login: termi-official
body: This should be queried in the asset download script and reference the local figure, right?
created_at: 2025-04-14T20:30:15Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    L2ProjectedDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: I assume we could also apply the H(curl)-conforming variant to ridges? If so, should we open a good-first-issue on this for potential contributors?
created_at: 2025-04-14T20:32:25Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    L2ProjectedDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
+
+A `L2ProjectedDirichlet` condition enforces conditions for `field_name` on the boundary, `facets`, for
+non-nodal interpolations (e.g. ``H(\mathrm{div})`` or ``H(\mathrm{curl})``) in an weak sense,
+i.e. minimizing the L2 error between the function `f(x, t, n)` and the values described
+by the FE approximation on the facet, ``\Gamma^f``. The arguments to the function are
+the coordinate, ``\boldsymbol{x}``, the time, ``t``, and the facet normal, ``\boldsymbol{n}``.
+The quadrature rule is automatically created, but the default order, `qr_order = 2 * ip_order`
+(may be refined in future releases),
+where `ip_order` is the order of the interpolation, can be overrided if desired.
+
+# H(div) interpolations
+For H(div), we want to prescribe the normal flux, ``q_\mathrm{n} = f(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n}) \right]^2 \mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Finding the stationary point by taking the directional derivative in all possible directions,
+``\delta\boldsymbol{q}(\boldsymbol{x})``, yields,
+```math
+0 = \lim_{\epsilon\rightarrow 0}\frac{\partial}{\partial \epsilon}
+U\big(\boldsymbol{q}(\boldsymbol{x}) + \epsilon \delta\boldsymbol{q}(\boldsymbol{x})\big)
+ = 2\int_{\Gamma^f} \left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n})\right]
+    \delta\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n}\ \mathrm{d}\Gamma
+```
+Inserting the FE-approximations,
+``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f`` and
+``\delta\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_i(\boldsymbol{x}) c_i^f``,
+and using the arbitrariness of ``c_i^f``, yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right]\left[\boldsymbol{N}^f_j \cdot \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right] f(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``.
+
+# H(curl) interpolations
+For H(curl), we want to prescribe the tangential flux, ``\boldsymbol{q}_\mathrm{t} = \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left\vert\left\vert
+    \boldsymbol{q}(\boldsymbol{x}) \times \boldsymbol{n} - \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})
+\right\vert\right\vert^2
+\mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Similar to for ``H(\mathrm{div})``, we find the minimum by setting the directional derivative equal to zero,
+which after inserting the FE-approximations yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot\left[\boldsymbol{N}^f_j \times \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``
+"""
+mutable struct L2ProjectedDirichlet
+    const f::Function
+    const facets::OrderedSet{FacetIndex}
+    const field_name::Symbol
+    const qr_order::Int
+    # Created during `add!`
+    fv::Union{Nothing, FacetValues}
path: src/Dofs/ConstraintHandler.jl
user: login: termi-official
body: Just to make sure: This differs from Dirichlet in the sense that it only works on subdomains with the same element type (i.e. 3D no mixed grids), right? Should we do this split automatically in `add!` as a followup and add an error for now?

I assume that this is not covered yet because the 3D elements are not merged yet?
created_at: 2025-04-14T20:35:29Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    L2ProjectedDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
+
+A `L2ProjectedDirichlet` condition enforces conditions for `field_name` on the boundary, `facets`, for
+non-nodal interpolations (e.g. ``H(\mathrm{div})`` or ``H(\mathrm{curl})``) in an weak sense,
+i.e. minimizing the L2 error between the function `f(x, t, n)` and the values described
+by the FE approximation on the facet, ``\Gamma^f``. The arguments to the function are
+the coordinate, ``\boldsymbol{x}``, the time, ``t``, and the facet normal, ``\boldsymbol{n}``.
+The quadrature rule is automatically created, but the default order, `qr_order = 2 * ip_order`
+(may be refined in future releases),
+where `ip_order` is the order of the interpolation, can be overrided if desired.
+
+# H(div) interpolations
+For H(div), we want to prescribe the normal flux, ``q_\mathrm{n} = f(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n}) \right]^2 \mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Finding the stationary point by taking the directional derivative in all possible directions,
+``\delta\boldsymbol{q}(\boldsymbol{x})``, yields,
+```math
+0 = \lim_{\epsilon\rightarrow 0}\frac{\partial}{\partial \epsilon}
+U\big(\boldsymbol{q}(\boldsymbol{x}) + \epsilon \delta\boldsymbol{q}(\boldsymbol{x})\big)
+ = 2\int_{\Gamma^f} \left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n})\right]
+    \delta\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n}\ \mathrm{d}\Gamma
+```
+Inserting the FE-approximations,
+``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f`` and
+``\delta\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_i(\boldsymbol{x}) c_i^f``,
+and using the arbitrariness of ``c_i^f``, yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right]\left[\boldsymbol{N}^f_j \cdot \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right] f(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``.
+
+# H(curl) interpolations
+For H(curl), we want to prescribe the tangential flux, ``\boldsymbol{q}_\mathrm{t} = \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left\vert\left\vert
+    \boldsymbol{q}(\boldsymbol{x}) \times \boldsymbol{n} - \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})
+\right\vert\right\vert^2
+\mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Similar to for ``H(\mathrm{div})``, we find the minimum by setting the directional derivative equal to zero,
+which after inserting the FE-approximations yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot\left[\boldsymbol{N}^f_j \times \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``
+"""
+mutable struct L2ProjectedDirichlet
+    const f::Function
+    const facets::OrderedSet{FacetIndex}
+    const field_name::Symbol
+    const qr_order::Int
+    # Created during `add!`
+    fv::Union{Nothing, FacetValues}
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: Will add support for that now that #1173 has been merged. 
created_at: 2025-04-15T10:41:06Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    L2ProjectedDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: I don't know if that arises from some derivations (I thought so initially, but doesn't seem like it based on what I found when writing the following: https://ferrite-fem.github.io/Ferrite.jl/previews/PR798/tutorials/maxwell_good_bad_ugly/#Theoretical-background)
created_at: 2025-04-15T10:42:33Z
author_association: MEMBER
diff_hunk: @@ -93,6 +93,21 @@ end
     Most examples make use of Dirichlet boundary conditions, for example [Heat
     Equation](@ref tutorial-heat-equation).
 
+## L2ProjectedDirichlet
+Some interpolations don't have nodal support points:
+``H(\mathrm{curl})`` interpolations, e.g., `Nedelec`, are associated to edges
+and ``H(\mathrm{div})`` interpolations, e.g. `RaviartThomas`, are associated to facets.
+While normal `Dirichlet` boundary conditions assume the existence of such nodal support points,
+Ferrite provides the `L2ProjectedDirichlet`, which instead finds the degree of freedom values that
+minimizes the L2-distance between the prescribed function, ``f(\boldsymbol{x},t,\boldsymbol{n})``,
+and the finite element interpolation space. Although standard interpolations are not currently supported,
+the figure below illustrates well the difference between applying a standard `Dirichlet` condition and an
+`L2ProjectedDirichlet` condition when the prescribed function cannot be described by the chosen FE-interpolation.
+
+![L2ProjectedDirichlet illustration](https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/L2ProjectedDirichlet.svg)
path: docs/src/topics/boundary_conditions.md
user: login: KnutAM
body: Not sure what the best approach here is. Currently, only assets are downloaded for the tutorials, but I guess it makes sense to download it instead. Adding an svg to source control really bloats the number of lines etc, so not so nice.

Perhaps possible to use something like
``````
```@example
using Downloads: download #hide
download(joinpath(@__DIR__, L2ProjectedDirichlet.svg), "https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/L2ProjectedDirichlet.svg") #hide
nothing #hide
```
![L2ProjectedDirichlet illustration](L2ProjectedDirichlet.svg)
``````

With #1037 would aid this by centralizing the asset location.

@fredrikekre: What are your views on how we should manage such assets in the non-tutorial part of the docs? 
created_at: 2025-04-15T10:54:52Z
author_association: MEMBER
diff_hunk: @@ -93,6 +93,21 @@ end
     Most examples make use of Dirichlet boundary conditions, for example [Heat
     Equation](@ref tutorial-heat-equation).
 
+## L2ProjectedDirichlet
+Some interpolations don't have nodal support points:
+``H(\mathrm{curl})`` interpolations, e.g., `Nedelec`, are associated to edges
+and ``H(\mathrm{div})`` interpolations, e.g. `RaviartThomas`, are associated to facets.
+While normal `Dirichlet` boundary conditions assume the existence of such nodal support points,
+Ferrite provides the `L2ProjectedDirichlet`, which instead finds the degree of freedom values that
+minimizes the L2-distance between the prescribed function, ``f(\boldsymbol{x},t,\boldsymbol{n})``,
+and the finite element interpolation space. Although standard interpolations are not currently supported,
+the figure below illustrates well the difference between applying a standard `Dirichlet` condition and an
+`L2ProjectedDirichlet` condition when the prescribed function cannot be described by the chosen FE-interpolation.
+
+![L2ProjectedDirichlet illustration](https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/L2ProjectedDirichlet.svg)
path: docs/src/topics/boundary_conditions.md
user: login: termi-official
body: > Adding an svg to source control [...]

Do we have git lfs configured? If so, then we can just add it via git lfs to treat it similar to binaries. 

created_at: 2025-04-15T11:02:12Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    L2ProjectedDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
+
+A `L2ProjectedDirichlet` condition enforces conditions for `field_name` on the boundary, `facets`, for
+non-nodal interpolations (e.g. ``H(\mathrm{div})`` or ``H(\mathrm{curl})``) in an weak sense,
+i.e. minimizing the L2 error between the function `f(x, t, n)` and the values described
+by the FE approximation on the facet, ``\Gamma^f``. The arguments to the function are
+the coordinate, ``\boldsymbol{x}``, the time, ``t``, and the facet normal, ``\boldsymbol{n}``.
+The quadrature rule is automatically created, but the default order, `qr_order = 2 * ip_order`
+(may be refined in future releases),
+where `ip_order` is the order of the interpolation, can be overrided if desired.
+
+# H(div) interpolations
+For H(div), we want to prescribe the normal flux, ``q_\mathrm{n} = f(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n}) \right]^2 \mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Finding the stationary point by taking the directional derivative in all possible directions,
+``\delta\boldsymbol{q}(\boldsymbol{x})``, yields,
+```math
+0 = \lim_{\epsilon\rightarrow 0}\frac{\partial}{\partial \epsilon}
+U\big(\boldsymbol{q}(\boldsymbol{x}) + \epsilon \delta\boldsymbol{q}(\boldsymbol{x})\big)
+ = 2\int_{\Gamma^f} \left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n})\right]
+    \delta\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n}\ \mathrm{d}\Gamma
+```
+Inserting the FE-approximations,
+``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f`` and
+``\delta\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_i(\boldsymbol{x}) c_i^f``,
+and using the arbitrariness of ``c_i^f``, yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right]\left[\boldsymbol{N}^f_j \cdot \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right] f(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``.
+
+# H(curl) interpolations
+For H(curl), we want to prescribe the tangential flux, ``\boldsymbol{q}_\mathrm{t} = \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left\vert\left\vert
+    \boldsymbol{q}(\boldsymbol{x}) \times \boldsymbol{n} - \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})
+\right\vert\right\vert^2
+\mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Similar to for ``H(\mathrm{div})``, we find the minimum by setting the directional derivative equal to zero,
+which after inserting the FE-approximations yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot\left[\boldsymbol{N}^f_j \times \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``
+"""
+mutable struct L2ProjectedDirichlet
+    const f::Function
+    const facets::OrderedSet{FacetIndex}
+    const field_name::Symbol
+    const qr_order::Int
+    # Created during `add!`
+    fv::Union{Nothing, FacetValues}
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: Had already added support for this :D, now it is also tested. 
created_at: 2025-04-18T12:16:35Z
author_association: MEMBER
diff_hunk: @@ -93,6 +93,21 @@ end
     Most examples make use of Dirichlet boundary conditions, for example [Heat
     Equation](@ref tutorial-heat-equation).
 
+## L2ProjectedDirichlet
+Some interpolations don't have nodal support points:
+``H(\mathrm{curl})`` interpolations, e.g., `Nedelec`, are associated to edges
+and ``H(\mathrm{div})`` interpolations, e.g. `RaviartThomas`, are associated to facets.
+While normal `Dirichlet` boundary conditions assume the existence of such nodal support points,
+Ferrite provides the `L2ProjectedDirichlet`, which instead finds the degree of freedom values that
+minimizes the L2-distance between the prescribed function, ``f(\boldsymbol{x},t,\boldsymbol{n})``,
+and the finite element interpolation space. Although standard interpolations are not currently supported,
+the figure below illustrates well the difference between applying a standard `Dirichlet` condition and an
+`L2ProjectedDirichlet` condition when the prescribed function cannot be described by the chosen FE-interpolation.
+
+![L2ProjectedDirichlet illustration](https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/L2ProjectedDirichlet.svg)
path: docs/src/topics/boundary_conditions.md
user: login: KnutAM
body: Added to download script, saving it to a new `downloaded_assets` directory which is ignored by git. 
created_at: 2025-04-18T13:37:03Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    L2ProjectedDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
+
+A `L2ProjectedDirichlet` condition enforces conditions for `field_name` on the boundary, `facets`, for
+non-nodal interpolations (e.g. ``H(\mathrm{div})`` or ``H(\mathrm{curl})``) by
+minimizing the L2 error between the function `f(x, t, n)` and the values described
+by the FE approximation on the facet, ``\Gamma^f``. The arguments to the function are
+the coordinate, ``\boldsymbol{x}``, the time, ``t``, and the facet normal, ``\boldsymbol{n}``.
+The quadrature rule is automatically created, but the default order, `qr_order = 2 * ip_order`
+(may be refined in future releases),
+where `ip_order` is the order of the interpolation, can be overrided if desired.
+
+# H(div) interpolations
+For H(div), we want to prescribe the normal flux, ``q_\mathrm{n} = f(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n}) \right]^2 \mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Finding the stationary point by taking the directional derivative in all possible directions,
+``\delta\boldsymbol{q}(\boldsymbol{x})``, yields,
+```math
+0 = \lim_{\epsilon\rightarrow 0}\frac{\partial}{\partial \epsilon}
+U\big(\boldsymbol{q}(\boldsymbol{x}) + \epsilon \delta\boldsymbol{q}(\boldsymbol{x})\big)
+ = 2\int_{\Gamma^f} \left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n})\right]
+    \delta\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n}\ \mathrm{d}\Gamma
+```
+Inserting the FE-approximations,
+``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f`` and
+``\delta\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_i(\boldsymbol{x}) c_i^f``,
+and using the arbitrariness of ``c_i^f``, yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right]\left[\boldsymbol{N}^f_j \cdot \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right] f(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``.
+
+# H(curl) interpolations
+For H(curl), we want to prescribe the tangential flux, ``\boldsymbol{q}_\mathrm{t} = \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left\vert\left\vert
+    \boldsymbol{q}(\boldsymbol{x}) \times \boldsymbol{n} - \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})
+\right\vert\right\vert^2
+\mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Similar to for ``H(\mathrm{div})``, we find the minimum by setting the directional derivative equal to zero,
+which after inserting the FE-approximations yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot\left[\boldsymbol{N}^f_j \times \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``
+"""
+mutable struct L2ProjectedDirichlet
path: src/Dofs/ConstraintHandler.jl
user: login: fredrikekre
body: I liked `WeakDirichlet`. I guess I missed the discussion about the name change. Would at least ` ProjectedDirichlet` be an option?
created_at: 2025-04-23T04:18:15Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,78 @@ function __to_components(c)
     return components
 end
 
+@doc raw"""
+    L2ProjectedDirichlet(field_name::Symbol, facets::AbstractSet{FacetIndex}, f::Function; qr_order = -1)
+
+A `L2ProjectedDirichlet` condition enforces conditions for `field_name` on the boundary, `facets`, for
+non-nodal interpolations (e.g. ``H(\mathrm{div})`` or ``H(\mathrm{curl})``) by
+minimizing the L2 error between the function `f(x, t, n)` and the values described
+by the FE approximation on the facet, ``\Gamma^f``. The arguments to the function are
+the coordinate, ``\boldsymbol{x}``, the time, ``t``, and the facet normal, ``\boldsymbol{n}``.
+The quadrature rule is automatically created, but the default order, `qr_order = 2 * ip_order`
+(may be refined in future releases),
+where `ip_order` is the order of the interpolation, can be overrided if desired.
+
+# H(div) interpolations
+For H(div), we want to prescribe the normal flux, ``q_\mathrm{n} = f(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n}) \right]^2 \mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Finding the stationary point by taking the directional derivative in all possible directions,
+``\delta\boldsymbol{q}(\boldsymbol{x})``, yields,
+```math
+0 = \lim_{\epsilon\rightarrow 0}\frac{\partial}{\partial \epsilon}
+U\big(\boldsymbol{q}(\boldsymbol{x}) + \epsilon \delta\boldsymbol{q}(\boldsymbol{x})\big)
+ = 2\int_{\Gamma^f} \left[\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n} - f(\boldsymbol{x}, t, \boldsymbol{n})\right]
+    \delta\boldsymbol{q}(\boldsymbol{x}) \cdot \boldsymbol{n}\ \mathrm{d}\Gamma
+```
+Inserting the FE-approximations,
+``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f`` and
+``\delta\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_i(\boldsymbol{x}) c_i^f``,
+and using the arbitrariness of ``c_i^f``, yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right]\left[\boldsymbol{N}^f_j \cdot \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \cdot \boldsymbol{n}^f\right] f(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``.
+
+# H(curl) interpolations
+For H(curl), we want to prescribe the tangential flux, ``\boldsymbol{q}_\mathrm{t} = \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})``.
+To that end, we want to find the degree of freedom values, ``a_j^f``, associated
+with the the facet that minimizes
+```math
+U(\boldsymbol{q}(\boldsymbol{x})) = \int_{\Gamma^f}
+\left\vert\left\vert
+    \boldsymbol{q}(\boldsymbol{x}) \times \boldsymbol{n} - \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})
+\right\vert\right\vert^2
+\mathrm{d}\Gamma
+```
+with ``\boldsymbol{q}(\boldsymbol{x}) = \boldsymbol{N}^f_j(\boldsymbol{x}) a_j^f``.
+Similar to for ``H(\mathrm{div})``, we find the minimum by setting the directional derivative equal to zero,
+which after inserting the FE-approximations yields the linear equation system,
+```math
+\underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot\left[\boldsymbol{N}^f_j \times \boldsymbol{n}^f\right]\ \mathrm{d}\Gamma}_{K^f_{ij}}\ a_j^f
+= \underbrace{\int_{\Gamma^f} \left[\delta\boldsymbol{N}^f_i \times \boldsymbol{n}^f\right]\cdot \boldsymbol{f}(\boldsymbol{x}, t, \boldsymbol{n})\ \mathrm{d}\Gamma}_{f^f_i}
+```
+determining the coefficients to be prescribed, ``a_j^f``
+"""
+mutable struct L2ProjectedDirichlet
path: src/Dofs/ConstraintHandler.jl
user: login: KnutAM
body: > I liked `WeakDirichlet`

Me too 😀 
The problem was that Hughes beat us with the name (https://github.com/Ferrite-FEM/Ferrite.jl/pull/1151#discussion_r2042015156).

I guess so, just added L2 to be consistent with the L2Projector, but agree that ProjectedDirichlet reads better - will update.
created_at: 2025-04-23T05:08:04Z
author_association: MEMBER
diff_hunk: @@ -21,3 +22,14 @@ for (file, url) in [
         Downloads.download(url, afile)
     end
 end
+
+# Topics
+const topics_directory = joinpath(@__DIR__, "src", "topics", "downloaded_assets")
+mkpath(topics_directory)
+
+for (file, url) in [
+        "ProjectedDirichlet.svg" => "https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/ProjectedDirichlet.svg",
path: docs/download_resources.jl
user: login: termi-official
body: The legend needs an update.
created_at: 2025-04-28T10:22:26Z
author_association: MEMBER
diff_hunk: @@ -21,3 +22,14 @@ for (file, url) in [
         Downloads.download(url, afile)
     end
 end
+
+# Topics
+const topics_directory = joinpath(@__DIR__, "src", "topics", "downloaded_assets")
+mkpath(topics_directory)
+
+for (file, url) in [
+        "ProjectedDirichlet.svg" => "https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/ProjectedDirichlet.svg",
path: docs/download_resources.jl
user: login: KnutAM
body: Fixed
created_at: 2025-04-28T14:57:00Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-02-14T19:07:10Z
body: ## [Codecov](https://app.codecov.io/gh/Ferrite-FEM/Ferrite.jl/pull/1151?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ferrite-FEM) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.02%. Comparing base [(`0b130ef`)](https://app.codecov.io/gh/Ferrite-FEM/Ferrite.jl/commit/0b130ef9c5e303fb7e3831849bb9696430f5bcdd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ferrite-FEM) to head [(`3ece145`)](https://app.codecov.io/gh/Ferrite-FEM/Ferrite.jl/commit/3ece145174f2e412ab2a849ddf60666eab430a5d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ferrite-FEM).
> Report is 1 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1151      +/-   ##
==========================================
+ Coverage   93.89%   94.02%   +0.13%     
==========================================
  Files          39       39              
  Lines        6436     6560     +124     
==========================================
+ Hits         6043     6168     +125     
+ Misses        393      392       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Ferrite-FEM/Ferrite.jl/pull/1151?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ferrite-FEM).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Ferrite-FEM).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: termi-official
created_at: 2025-02-14T19:20:05Z
body: > I also chose to introduce the internal function_space(::Interpolation)::Val function, as this was useful for dispatching. 

Yea, as already discussed before, I think having a trait system will be useful in the future.

> @termi-official: RannacherTurek and CrouzeixRaviart should be L2, right?

Yes.
author_association: MEMBER
user: login: termi-official
created_at: 2025-02-17T12:50:34Z
body: Since I just saw it, should we rename `FunctionSpace` trait? Although technically correct, it might a bit misleading/ambiguous here. I think we rather want to describe something like conformity or continuity trait, right?
author_association: MEMBER
user: login: KnutAM
created_at: 2025-02-17T16:02:48Z
body: > Since I just saw it, should we rename FunctionSpace trait? Although technically correct, it might a bit misleading/ambiguous here. I think we rather want to describe something like conformity or continuity trait, right?

Yes, seems like `dealii` agrees with you: https://dealii.org/current/doxygen/deal.II/classFiniteElementData.html#a0cd5f34c3ab828fac31004f3b52921a1 :) 
author_association: MEMBER
user: login: KnutAM
created_at: 2025-02-17T19:28:32Z
body: > I haven't gone through the math but if it is sound and tests cover it then 👍

I believe it is sound, but would be good if someone can have a 2nd look on the tests [here](https://github.com/Ferrite-FEM/Ferrite.jl/blob/04f7cdb57c16c36cf65d26afd30c92b9e3f73cda/test/test_interpolations.jl#L427-L519) to make sure that I haven't done anything wrong there. 

When going through this again now, I realize that I should also test some moment-integrals, as currently only the total integral quantity is tested (e.g. the total flux over the boundary, but not the spatial distribution). Will update with that during the week. 
author_association: MEMBER
user: login: KnutAM
created_at: 2025-02-18T14:22:32Z
body: > Is it too magical if Dirichlet(::HcurlInterpolation) adds a WeakDirichlet? Probably, but would be kind of nice to not have to change the BC when changing interpolations.

1) We might want to add `WeakDirichlet` to `:H1` interpolations in the future (see "side note" in the first comment). 
2) We might want to use `Dirichlet` for the method they have in dealii (@termi-official has some contacts that could help us figure out their exact implementation)


author_association: MEMBER
user: login: KnutAM
created_at: 2025-04-14T15:03:52Z
body: > I was asking myself if this infrastructure can help us with applying normal and tangential boundary conditions (e.g. static sliding contact)?

No, that would require redefining the coordinate system for the dofs, or introduce affine constraints. #1158 should make the latter quite acceptable to solve for users. 
author_association: MEMBER
user: login: KnutAM
created_at: 2025-04-23T13:02:00Z
body: Thanks to @termi-official on Slack - it is not a local projection problem for 3D and H(curl), so will add an error message that this is not implemented. 
There is nothing preventing it being implemented, but that gives the same requirements as for H1 that the equation system to be solved is not local. 
author_association: MEMBER
user: login: KnutAM
created_at: 2025-04-24T06:28:10Z
body: @lijas identified the corresponding functionality in deal.ii! 

For nodal and hdiv: https://www.dealii.org/current/doxygen/deal.II/namespaceVectorTools.html#a747e71d426643718d52eda904d46bb20

And for hcurl: https://www.dealii.org/current/doxygen/deal.II/group__constraints.html#ga190b302dc1ee7c89727c94c083d132d2

I will look closer if I'm missing something since they have factored out the curl-functionality, but I haven't done that here. 
author_association: MEMBER
user: login: KnutAM
created_at: 2025-04-27T11:11:12Z
body: After going through the dealii docs, I've added the relevant reference here: https://ferrite-fem.github.io/Ferrite.jl/previews/PR1151/topics/boundary_conditions/#ProjectedDirichlet. 

For the H(curl), AFAIU this is related to @termi-official's comment regarding 3d, and they use a stepwise algorithm to reduce the equation system size when it couples (first solving dofs associated with edges, and then those associated with faces). This doesn't affect what is implemented in this PR, nor the user interface.
author_association: MEMBER
user: login: KnutAM
created_at: 2025-04-27T12:25:17Z
body: Should be good from my side. 
Do you have any further comments/suggestions @termi-official or @fredrikekre?
author_association: MEMBER
user: login: KnutAM
created_at: 2025-04-30T13:35:25Z
body: Thanks for the comments and input @termi-official, @fredrikekre, and @lijas!
author_association: MEMBER