Two levels of "LazyMapOver" because first we LazyMapOver the Tuple of argument of the linear form, and the for each item of this Tuple we LazyMapOver the shape functions.
From tuples $a=(a_1, a_2, …, a_i, …, a_m)$ and $b=(b_1, b_2, …, b_j, …, b_n)$, it builds A and B which correspond formally to the following two matrices :
Two levels of "LazyMapOver" because first we LazyMapOver the Tuple of argument of the linear form, and the for each item of this Tuple we LazyMapOver the shape functions.
From tuples $a=(a_1, a_2, …, a_i, …, a_m)$ and $b=(b_1, b_2, …, b_j, …, b_n)$, it builds A and B which correspond formally to the following two matrices :
A and B are wrapped in LazyMapOver structures so that all operations on them are done elementwise by default (in other words, it can be considered that the operations are automatically broadcasted).
Dev note :
Both A and B are stored as a tuple of tuples, wrapped by LazyMapOver, where inner tuples correspond to each columns of a matrix. This hierarchical structure reduces both inference and compile times by avoiding the use of large tuples.
Count the (maximum) number of elements in the matrix corresponding to the bilinear assembly of U, V on a cell domain, where U and V are TrialFESpace and TestFESpace
Count the (maximum) number of elements in the matrix corresponding to the bilinear assembly of U, V on a cell domain, where U and V are AbstractMultiFESpace
_diag_tuples(diag::Tuple{Vararg{Any,N}}, b) where N
Return N tuples of length N. For each tuple tᵢ, its values are defined so that tᵢ[k]=diag[k] if k==i, tᵢ[k]=b otherwise. The result can be seen as a dense diagonal-like array using tuple.
Example for N=3:
(diag[1], b, b ),
+\end{pmatrix}\]
A and B are wrapped in LazyMapOver structures so that all operations on them are done elementwise by default (in other words, it can be considered that the operations are automatically broadcasted).
Dev note :
Both A and B are stored as a tuple of tuples, wrapped by LazyMapOver, where inner tuples correspond to each columns of a matrix. This hierarchical structure reduces both inference and compile times by avoiding the use of large tuples.
Count the (maximum) number of elements in the matrix corresponding to the bilinear assembly of U, V on a cell domain, where U and V are TrialFESpace and TestFESpace
Count the (maximum) number of elements in the matrix corresponding to the bilinear assembly of U, V on a cell domain, where U and V are AbstractMultiFESpace
_diag_tuples(diag::Tuple{Vararg{Any,N}}, b) where N
Return N tuples of length N. For each tuple tᵢ, its values are defined so that tᵢ[k]=diag[k] if k==i, tᵢ[k]=b otherwise. The result can be seen as a dense diagonal-like array using tuple.
Example for N=3:
(diag[1], b, b ),
(b, diag[2], b ),
-(b, b, diag[3]))
Return blockU and blockV to be able to compute the local matrix corresponding to the bilinear form :
\[ A[i,j] = a(λᵤ[j], λᵥ[i])\]
where λᵤ and λᵥ are the shape functions associated with the trial U and the test V function spaces respectively. In a "map-over" version, it can be written :
\[ A = a(blockU, blockV)\]
where blockU and blockV correspond formally to the lazy-map-over matrices :
Return all shape functions a = LazyMapOver((λ₁, λ₂, …, λₙ)) corresponding to fespace in cell cellinfo. These shape functions are wrapped by a LazyMapOver so that for a function f it gives: f(a) == map(f, a)
Return blockU and blockV to be able to compute the local matrix corresponding to the bilinear form :
\[ A[i,j] = a(λᵤ[j], λᵥ[i])\]
where λᵤ and λᵥ are the shape functions associated with the trial U and the test V function spaces respectively. In a "map-over" version, it can be written :
\[ A = a(blockU, blockV)\]
where blockU and blockV correspond formally to the lazy-map-over matrices :
Return all shape functions a = LazyMapOver((λ₁, λ₂, …, λₙ)) corresponding to fespace in cell cellinfo. These shape functions are wrapped by a LazyMapOver so that for a function f it gives: f(a) == map(f, a)
Apply quadrature rule to function g_ref expressed on reference shape shape. Computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
Integrate function g expressed in local element. Depending on the cell type and the space dimension, a volumic or a 'surfacic' integration is performed.
integrate_on_ref(g, cellinfo::CellInfo, quadrature::AbstractQuadrature, [::T]) where {N,[T<:AbstractComputeQuadratureStyle]}
Integrate a function g over a cell decribed by cellinfo. The function g can be expressed in the reference or the physical space corresponding to the cell, both cases are automatically handled by applying necessary mapping when needed.
This function is helpfull to integrate shape functions (for instance $\int \lambda_i \lambda_j$) when the inverse mapping is not known explicitely (hence only $\hat{lambda}$ are known, not $\lambda$).
If the last argument is given, computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
integrate_ref(g_ref, cnodes, ctype::AbstractEntityType, quadrature::AbstractQuadrature, [::T]) where {[T<:AbstractComputeQuadratureStyle]}
Integrate function g_ref expressed in reference element. A variable substitution (involving Jacobian & Cie) is still applied, but the function is considered to be already mapped.
This function is helpfull to integrate shape functions (for instance $\int \lambda_i \lambda_j$) when the inverse mapping is not known explicitely (hence only $\hat{lambda}$ are known, not $\lambda$).
If the last argument is given, computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
Integrate function g_ref on the iside-th side of the cell defined by its nodes cnodes and its type ctype. Function g_ref(x) is expressed in the cell-reference element (not the face reference).
This function is helpfull to integrate shape functions (for instance $\int \lambda_i \lambda_j$) when the inverse mapping is not known explicitely (hence only $\hat{lambda}$ are known, not $\lambda$).
integrate_ref(::isCurvilinear, g_ref, cnodes, ctype::AbstractEntityType{1}, quadrature::AbstractQuadrature, ::T) where {T<:AbstractComputeQuadratureStyle}
Perform an integration of the function g_ref (expressed in local element) over a line in a $\matbb{R}^n$ space.
The applied formulae is: $\int_\Gamma g(x) dx = \int_l ||F'(l)|| g_ref(l) dl$ where $F ~:~ \mathbb{R} \rightarrow \mathbb{R}^n$ is the reference segment [-1,1] to the R^n line mapping.
Computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
Integration on a node in a $\mathbb{R}^n$ space. This trivial function is only to simplify the 'side integral' expression.
Implementation
For consistency reasons, g_ref is a function but it doesnt actually use its argument : the "reference-element" of a Node can be anything. For instance consider integrating g(x) = x on a node named node. Then g_ref(ξ) = g ∘ node.x. As you can see, g_ref doesnt actually depend on ξ
Integrate function g_ref (expressed in reference element) on mesh element of type ctype defined by its cnodes at the quadrature. Computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
To do so, a variable substitution is performed to integrate on the reference element.
Implementation
It has been checked that calling the apply_quadrature method within this function instead of directly applying the quadrature rule (i.e without the anonymous function) does not decrease performance nor allocation.
Abstract type representing a quadrature node for a shape S and a quadrature Q. This type is used to represent and identify easily a quadrature node in a quadrature rules.
Derived types must implement the following method:
Abstract type representing a quadrature rule for a shape S and quadrature Q.
Derived types must implement the following method: - [get_weights(qr::AbstractQuadratureRule)] - [get_nodes(qr::AbstractQuadratureRule)] - [Base.length(qr::AbstractQuadratureRule)]
Type representing a quadrature node for a shape S and a quadrature Q. This type can be used to represent and identify easily a quadrature node in the corresponding parent quadrature rule.
Gauss-Legendre formula with $n$ nodes has degree of exactness $2n-1$. Then, to obtain a given degree $D$, the number of nodes must satisfy: $2n-1 ≥ D$ or equivalently $n ≥ (D+1)/2$
Gauss-Lobatto formula with $n+1$ nodes has degree of exactness $2n-1$, which equivalent to a degree of $2n-3$ with $n$ nodes. Then, to obtain a given degree $D$, the number of nodes must satisfy: $2n-3 ≥ D$ or equivalently $n ≥ (D+3)/2$
get_num_nodes_per_dim(quadrule::AbstractQuadratureRule{S}) where S<:Shape
Returns the number of nodes per dimension. This function is defined for shapes for which quadratures are based on a cartesian product : Line, Square, Cube
Remark : Here we assume that the same degree is used along each dimension (no anisotropy for now!)
Gauss-Legendre quadrature, 12 point rule on triangle.
Ref: Witherden, F. D.; Vincent, P. E. On the identification of symmetric quadrature rules for finite element methods. Comput. Math. Appl. 69 (2015), no. 10, 1232–1241
Gauss-Legendre quadrature, 16 point rule on triangle, degree 8.
Ref: Witherden, F. D.; Vincent, P. E. On the identification of symmetric quadrature rules for finite element methods. Comput. Math. Appl. 69 (2015), no. 10, 1232–1241
Note : quadrature is rescale to match our reference triangular shape which is defined in [0:1]² instead of [-1:1]²
quadrature_rule_bary(::Int, ::AbstractShape, degree::Val{N}) where N
Return the quadrature rule, computed with barycentric coefficients, corresponding to the given boundary of a shape and the given degree.
This function returns the quadrature weights and the barycentric weights to apply to each vertex of the reference shape. Hence, to apply the quadrature using this function, one needs to do : for (weight, l) in quadrature_rule_bary(iside, shape(etype), degree) xp = zeros(SVector{td}) for i=1:nvertices xp += l[i]*vertices[i] end # weight, xp is the quadrature couple (weight, node) end
Apply quadrature rule to function g_ref expressed on reference shape shape. Computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
Integrate function g expressed in local element. Depending on the cell type and the space dimension, a volumic or a 'surfacic' integration is performed.
integrate_on_ref(g, cellinfo::CellInfo, quadrature::AbstractQuadrature, [::T]) where {N,[T<:AbstractComputeQuadratureStyle]}
Integrate a function g over a cell decribed by cellinfo. The function g can be expressed in the reference or the physical space corresponding to the cell, both cases are automatically handled by applying necessary mapping when needed.
This function is helpfull to integrate shape functions (for instance $\int \lambda_i \lambda_j$) when the inverse mapping is not known explicitely (hence only $\hat{lambda}$ are known, not $\lambda$).
If the last argument is given, computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
integrate_ref(g_ref, cnodes, ctype::AbstractEntityType, quadrature::AbstractQuadrature, [::T]) where {[T<:AbstractComputeQuadratureStyle]}
Integrate function g_ref expressed in reference element. A variable substitution (involving Jacobian & Cie) is still applied, but the function is considered to be already mapped.
This function is helpfull to integrate shape functions (for instance $\int \lambda_i \lambda_j$) when the inverse mapping is not known explicitely (hence only $\hat{lambda}$ are known, not $\lambda$).
If the last argument is given, computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
Integrate function g_ref on the iside-th side of the cell defined by its nodes cnodes and its type ctype. Function g_ref(x) is expressed in the cell-reference element (not the face reference).
This function is helpfull to integrate shape functions (for instance $\int \lambda_i \lambda_j$) when the inverse mapping is not known explicitely (hence only $\hat{lambda}$ are known, not $\lambda$).
integrate_ref(::isCurvilinear, g_ref, cnodes, ctype::AbstractEntityType{1}, quadrature::AbstractQuadrature, ::T) where {T<:AbstractComputeQuadratureStyle}
Perform an integration of the function g_ref (expressed in local element) over a line in a $\matbb{R}^n$ space.
The applied formulae is: $\int_\Gamma g(x) dx = \int_l ||F'(l)|| g_ref(l) dl$ where $F ~:~ \mathbb{R} \rightarrow \mathbb{R}^n$ is the reference segment [-1,1] to the R^n line mapping.
Computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
Integration on a node in a $\mathbb{R}^n$ space. This trivial function is only to simplify the 'side integral' expression.
Implementation
For consistency reasons, g_ref is a function but it doesnt actually use its argument : the "reference-element" of a Node can be anything. For instance consider integrating g(x) = x on a node named node. Then g_ref(ξ) = g ∘ node.x. As you can see, g_ref doesnt actually depend on ξ
Integrate function g_ref (expressed in reference element) on mesh element of type ctype defined by its cnodes at the quadrature. Computation is optimized according to the given concrete type T<:AbstractComputeQuadratureStyle.
To do so, a variable substitution is performed to integrate on the reference element.
Implementation
It has been checked that calling the apply_quadrature method within this function instead of directly applying the quadrature rule (i.e without the anonymous function) does not decrease performance nor allocation.
Abstract type representing a quadrature node for a shape S and a quadrature Q. This type is used to represent and identify easily a quadrature node in a quadrature rules.
Derived types must implement the following method:
Abstract type representing a quadrature rule for a shape S and quadrature Q.
Derived types must implement the following method: - [get_weights(qr::AbstractQuadratureRule)] - [get_nodes(qr::AbstractQuadratureRule)] - [Base.length(qr::AbstractQuadratureRule)]
Type representing a quadrature node for a shape S and a quadrature Q. This type can be used to represent and identify easily a quadrature node in the corresponding parent quadrature rule.
Gauss-Legendre formula with $n$ nodes has degree of exactness $2n-1$. Then, to obtain a given degree $D$, the number of nodes must satisfy: $2n-1 ≥ D$ or equivalently $n ≥ (D+1)/2$
Gauss-Lobatto formula with $n+1$ nodes has degree of exactness $2n-1$, which equivalent to a degree of $2n-3$ with $n$ nodes. Then, to obtain a given degree $D$, the number of nodes must satisfy: $2n-3 ≥ D$ or equivalently $n ≥ (D+3)/2$
get_num_nodes_per_dim(quadrule::AbstractQuadratureRule{S}) where S<:Shape
Returns the number of nodes per dimension. This function is defined for shapes for which quadratures are based on a cartesian product : Line, Square, Cube
Remark : Here we assume that the same degree is used along each dimension (no anisotropy for now!)
Gauss-Legendre quadrature, 12 point rule on triangle.
Ref: Witherden, F. D.; Vincent, P. E. On the identification of symmetric quadrature rules for finite element methods. Comput. Math. Appl. 69 (2015), no. 10, 1232–1241
Gauss-Legendre quadrature, 16 point rule on triangle, degree 8.
Ref: Witherden, F. D.; Vincent, P. E. On the identification of symmetric quadrature rules for finite element methods. Comput. Math. Appl. 69 (2015), no. 10, 1232–1241
Note : quadrature is rescale to match our reference triangular shape which is defined in [0:1]² instead of [-1:1]²
quadrature_rule_bary(::Int, ::AbstractShape, degree::Val{N}) where N
Return the quadrature rule, computed with barycentric coefficients, corresponding to the given boundary of a shape and the given degree.
This function returns the quadrature weights and the barycentric weights to apply to each vertex of the reference shape. Hence, to apply the quadrature using this function, one needs to do : for (weight, l) in quadrature_rule_bary(iside, shape(etype), degree) xp = zeros(SVector{td}) for i=1:nvertices xp += l[i]*vertices[i] end # weight, xp is the quadrature couple (weight, node) end
Alternatively, you may define a "parent" to your structure by implementing the Base.parent function. Then, all the above functions will be redirected to the "parent" FESpace.
Alternatively, you may define a "parent" to your structure by implementing the Base.parent function. Then, all the above functions will be redirected to the "parent" FESpace.
Build a finite element space representing several sub- finite element spaces.
This is particulary handy when several variables are in play since it provides a global dof numbering (for the whole system). The finite element spaces composing the MultiFESpace can be different from each other (some continuous, some discontinuous, some scalar, some vectors...).
Arguments
feSpaces : the finite element spaces composing the MultiFESpace. Note that they must be of type TrialFESpace or TestFESpace.
Keywords
arrayOfStruct::Bool = AOS_DEFAULT : indicates if the dof numbering should be of type "Array of Structs" (AoS) or "Struct of Arrays" (SoA).
Build a finite element space representing several sub- finite element spaces.
This is particulary handy when several variables are in play since it provides a global dof numbering (for the whole system). The finite element spaces composing the MultiFESpace can be different from each other (some continuous, some discontinuous, some scalar, some vectors...).
Arguments
feSpaces : the finite element spaces composing the MultiFESpace. Note that they must be of type TrialFESpace or TestFESpace.
Keywords
arrayOfStruct::Bool = AOS_DEFAULT : indicates if the dof numbering should be of type "Array of Structs" (AoS) or "Struct of Arrays" (SoA).
An finite-element space (FESpace) is basically a function space, associated to degrees of freedom (on a mesh).
A FESpace can be either scalar (to represent a Temperature for instance) or vector (to represent a Velocity). In case of a "vector" SingleFESpace, all the components necessarily share the same FunctionSpace.
An finite-element space (FESpace) is basically a function space, associated to degrees of freedom (on a mesh).
A FESpace can be either scalar (to represent a Temperature for instance) or vector (to represent a Velocity). In case of a "vector" SingleFESpace, all the components necessarily share the same FunctionSpace.
A TestFESpace can be built from a TrialFESpace. See SingleFESpace for hints about the function arguments. Only arguments specific to TrialFESpace are detailed below.
Examples
julia> mesh = one_cell_mesh(:line)
julia> fSpace = FunctionSpace(:Lagrange, 2)
julia> U = TrialFESpace(fSpace, mesh)
-julia> V = TestFESpace(U)
allocate_dofs(feSpace::AbstractFESpace, T = Float64)
Allocate a vector with a size equal to the number of dof of the FESpace, with the type T. For a MultiFESpace, a vector of the total size of the space is returned (and not a Tuple of vectors)
Return the dofs indices for the cell icell for each single-feSpace. Result is a tuple of array of integers, where each array of integers are the indices relative to the numbering of each singleFESpace.
Warning:
Combine get_dofs with get_mapping if global dofs indices are needed.
Return the i-th FESpace composing this AbstractMultiFESpace. If no index is provided, the tuple of FESpace composing this AbstractMultiFESpace` is returnted.
Return the total number of dofs of the FESpace, taking into account the continuous/discontinuous type of the space. If the FESpace contains itself several FESpace (see MultiFESpace), the sum of all dofs is returned.
allocate_dofs(feSpace::AbstractFESpace, T = Float64)
Allocate a vector with a size equal to the number of dof of the FESpace, with the type T. For a MultiFESpace, a vector of the total size of the space is returned (and not a Tuple of vectors)
Return the dofs indices for the cell icell for each single-feSpace. Result is a tuple of array of integers, where each array of integers are the indices relative to the numbering of each singleFESpace.
Warning:
Combine get_dofs with get_mapping if global dofs indices are needed.
Return the i-th FESpace composing this AbstractMultiFESpace. If no index is provided, the tuple of FESpace composing this AbstractMultiFESpace` is returnted.
Return the total number of dofs of the FESpace, taking into account the continuous/discontinuous type of the space. If the FESpace contains itself several FESpace (see MultiFESpace), the sum of all dofs is returned.
Return the local indices of the dofs lying on each vertex of the Shape.
Beware that we are talking about the Shape, not the EntityType. So 'interior' vertices of the EntityType are not taken into account for instance. See Lagrange interpolation for simple examples.
shape_functions(::AbstractFunctionSpace, ::Val{N}, shape::AbstractShape, ξ) where N
-shape_functions(::AbstractFunctionSpace, shape::AbstractShape, ξ)
Return the list of shape functions corresponding to a FunctionSpace and a Shape. N is the size of the finite element space (default: N=1 if the argument is not provided).
The result is a vector of all the shape functions evaluated at position ξ, and not a tuple of the different shape functions. This choice is optimal for performance.
Note : λ = ξ -> shape_functions(fs, shape, ξ); λ(ξ)[i] is faster than λ =shape_functions(fs, shape); λ[i](ξ)
Implementation
Default version, should be overriden for each concrete FunctionSpace.
Returns an array containing the values of f interpolated to new DoFs on fdomain. The DoFs locations on fdomain correspond to those of a discontinuous FESpace with a :Lagrange function space of selected degree.
Returns an array containing the values of f interpolated to new DoFs. The DoFs correspond to those of a discontinuous cell variable with a :Lagrange function space of selected degree.
Return the local indices of the dofs lying on each vertex of the Shape.
Beware that we are talking about the Shape, not the EntityType. So 'interior' vertices of the EntityType are not taken into account for instance. See Lagrange interpolation for simple examples.
shape_functions(::AbstractFunctionSpace, ::Val{N}, shape::AbstractShape, ξ) where N
+shape_functions(::AbstractFunctionSpace, shape::AbstractShape, ξ)
Return the list of shape functions corresponding to a FunctionSpace and a Shape. N is the size of the finite element space (default: N=1 if the argument is not provided).
The result is a vector of all the shape functions evaluated at position ξ, and not a tuple of the different shape functions. This choice is optimal for performance.
Note : λ = ξ -> shape_functions(fs, shape, ξ); λ(ξ)[i] is faster than λ =shape_functions(fs, shape); λ[i](ξ)
Implementation
Default version, should be overriden for each concrete FunctionSpace.
Returns an array containing the values of f interpolated to new DoFs on fdomain. The DoFs locations on fdomain correspond to those of a discontinuous FESpace with a :Lagrange function space of selected degree.
Returns an array containing the values of f interpolated to new DoFs. The DoFs correspond to those of a discontinuous cell variable with a :Lagrange function space of selected degree.
Return the index of the vertices on the iside-th face of a shape. If side is positive, the face is oriented preserving the cell normal. If side is negative, the face is returned with the opposite direction (i.e reverse node order).
Return the index of the vertices on the iside-th face of a shape. If side is positive, the face is oriented preserving the cell normal. If side is negative, the face is returned with the opposite direction (i.e reverse node order).
Default version : the shape functions are "replicated". If shape_functions returns the vector [λ₁; λ₂; λ₃], and if the FESpace is of size 2, then this default behaviour consists in returning the matrix [λ₁ 0; λ₂ 0; λ₃ 0; 0 λ₁; 0 λ₂; 0 λ₃].
Note that a Taylor-P0 is strictly equivalent to a 1st-order Finite Volume discretization (beware that "order" can have different meaning depending on whether one refers to the order of the function space basis or the order of the discretization method).
Recall that any function space implies that any function $g$ is interpolated by $g(x) = \sum g_i \lambda_i(x)$ where $\lambda_i$ are the shape functions. For a Taylor expansion, the definition of $\lambda_i$ is not unique. For instance for the Taylor expansion of order $1$ on a 1D line above, we may be tempted to set $\lambda_1(x) = 1$ and $\lambda_2(x) = (x - x_0)$. If you do so, what are the corresponding shape functions in the reference element, the $\hat{\lambda_i}$? We immediately recover $\hat{\lambda_1}(\hat{x}) = 1$. For $\hat{\lambda_2}$:
So if you set $\lambda_2(x) = (x - x_0)$ then $\hat{\lambda_2}$ depends on the element length ($\Delta x = x_r-x_l$), which is pointless. So $\lambda_2$ must be proportional to the element length to obtain a universal definition for $\hat{\lambda_2}$. For instance, we may choose $\lambda_2(x) = (x - x_0) / \Delta x$, leading to $\hat{\lambda_2}(\hat{x}) = \hat{x} / 2$. But we could have chosen an other element length multiple.
Don't forget that choosing $\lambda_2(x) = (x - x_0) / \Delta x$ leads to $g(x) = g(x_0) + \frac{x - x_0}{\Delta x} g'(x_0) Δx$ hence $g_2 = g'(x_0) Δx$ in the interpolation.
Default version : the shape functions are "replicated". If shape_functions returns the vector [λ₁; λ₂; λ₃], and if the FESpace is of size 2, then this default behaviour consists in returning the matrix [λ₁ 0; λ₂ 0; λ₃ 0; 0 λ₁; 0 λ₂; 0 λ₃].
Note that a Taylor-P0 is strictly equivalent to a 1st-order Finite Volume discretization (beware that "order" can have different meaning depending on whether one refers to the order of the function space basis or the order of the discretization method).
Recall that any function space implies that any function $g$ is interpolated by $g(x) = \sum g_i \lambda_i(x)$ where $\lambda_i$ are the shape functions. For a Taylor expansion, the definition of $\lambda_i$ is not unique. For instance for the Taylor expansion of order $1$ on a 1D line above, we may be tempted to set $\lambda_1(x) = 1$ and $\lambda_2(x) = (x - x_0)$. If you do so, what are the corresponding shape functions in the reference element, the $\hat{\lambda_i}$? We immediately recover $\hat{\lambda_1}(\hat{x}) = 1$. For $\hat{\lambda_2}$:
So if you set $\lambda_2(x) = (x - x_0)$ then $\hat{\lambda_2}$ depends on the element length ($\Delta x = x_r-x_l$), which is pointless. So $\lambda_2$ must be proportional to the element length to obtain a universal definition for $\hat{\lambda_2}$. For instance, we may choose $\lambda_2(x) = (x - x_0) / \Delta x$, leading to $\hat{\lambda_2}(\hat{x}) = \hat{x} / 2$. But we could have chosen an other element length multiple.
Don't forget that choosing $\lambda_2(x) = (x - x_0) / \Delta x$ leads to $g(x) = g(x_0) + \frac{x - x_0}{\Delta x} g'(x_0) Δx$ hence $g_2 = g'(x_0) Δx$ in the interpolation.
Default version : the shape functions are "replicated". If shape_functions returns the vector [λ₁; λ₂; λ₃], and if the FESpace is of size 2, then this default behaviour consists in returning the matrix [λ₁ 0; λ₂ 0; λ₃ 0; 0 λ₁; 0 λ₂; 0 λ₃].