Skip to content

Commit e433560

Browse files
authored
Add Lederer-Schoberl element (#301)
* add lowest degree Lederer-Schoberl element * ruff * higher order element on triangles * Fix element in 3D * mypy * update docs * co_contravariant * cahce * changelog
1 parent 325bced commit e433560

File tree

8 files changed

+308
-16
lines changed

8 files changed

+308
-16
lines changed

CHANGELOG_SINCE_LAST_VERSION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
- Allow "gl" variant of Lagrange element
22
- Implement component-wise integrals of VectorFunction and MatrixFunction
33
- Implement grad of VectorFunction
4+
- Add Lederer-Schoberl element

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ The reference triangle has vertices (0, 0), (1, 0), and (0, 1). Its sub-entities
187187
- Hsieh-Clough-Tocher (alternative names: Clough-Tocher, HCT, CT)
188188
- Kong-Mulder-Veldhuizen (alternative names: KMV)
189189
- Lagrange (alternative names: P)
190+
- Lederer-Schoberl
190191
- Mardal-Tai-Winther (alternative names: MTW)
191192
- matrix Lagrange
192193
- Morley
@@ -258,6 +259,7 @@ The reference tetrahedron has vertices (0, 0, 0), (1, 0, 0), (0, 1, 0), and (0,
258259
- Hermite
259260
- Kong-Mulder-Veldhuizen (alternative names: KMV)
260261
- Lagrange (alternative names: P)
262+
- Lederer-Schoberl
261263
- Mardal-Tai-Winther (alternative names: MTW)
262264
- matrix Lagrange
263265
- Morley-Wang-Xu (alternative names: MWX)

symfem/create.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ def create_element(
178178
enriched Galerkin, EG,
179179
enriched vector Galerkin, locking-free enriched Galerkin, LFEG,
180180
P1 macro,
181-
Alfeld-Sorokina, AS
181+
Alfeld-Sorokina, AS,
182+
Lederer-Schoberl
182183
order: The order of the element.
183184
vertices: The vertices of the reference.
184185
"""
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Lederer-Schoberl elements on simplices.
2+
3+
This element's definition appears in https://doi.org/10.34726/hss.2019.62042
4+
(Lederer, 2019).
5+
"""
6+
7+
import typing
8+
9+
10+
from symfem.elements.lagrange import Lagrange
11+
from symfem.finite_element import CiarletElement
12+
from symfem.functionals import (
13+
InnerProductIntegralMoment,
14+
IntegralMoment,
15+
TraceIntegralMoment,
16+
ListOfFunctionals,
17+
)
18+
from symfem.functions import FunctionInput, MatrixFunction
19+
from symfem.moments import make_integral_moment_dofs
20+
from symfem.polynomials import polynomial_set_vector
21+
from symfem.references import Reference
22+
from symfem.symbols import t, x
23+
24+
__all__ = ["LedererSchoberl"]
25+
26+
27+
class LedererSchoberl(CiarletElement):
28+
"""A Lederer-Schoberl element on a simplex."""
29+
30+
def __init__(self, reference: Reference, order: int):
31+
"""Create the element.
32+
33+
Args:
34+
reference: The reference element
35+
order: The polynomial order
36+
variant: The variant of the element
37+
"""
38+
from symfem import create_reference
39+
40+
tdim = reference.tdim
41+
poly: typing.List[FunctionInput] = [
42+
tuple(tuple(p[i * tdim : (i + 1) * tdim]) for i in range(tdim))
43+
for p in polynomial_set_vector(tdim, tdim**2, order)
44+
]
45+
46+
dofs: ListOfFunctionals = []
47+
facet_dim = reference.tdim - 1
48+
space = Lagrange(
49+
create_reference(["point", "interval", "triangle"][facet_dim]), order, "equispaced"
50+
)
51+
basis = [f.subs(x, t) for f in space.get_basis_functions()]
52+
for facet_n in range(reference.sub_entity_count(facet_dim)):
53+
facet = reference.sub_entity(facet_dim, facet_n)
54+
for tangent in facet.axes:
55+
for f, dof in zip(basis, space.dofs):
56+
dofs.append(
57+
InnerProductIntegralMoment(
58+
reference,
59+
f,
60+
tuple(i / facet.volume() for i in tangent),
61+
facet.normal(),
62+
dof,
63+
entity=(facet_dim, facet_n),
64+
mapping="co_contravariant",
65+
)
66+
)
67+
68+
dofs += make_integral_moment_dofs(
69+
reference,
70+
cells=(
71+
TraceIntegralMoment,
72+
Lagrange,
73+
order,
74+
"co_contravariant",
75+
),
76+
)
77+
78+
if order > 0:
79+
if reference.tdim == 2:
80+
functions = [
81+
MatrixFunction(i)
82+
for i in [
83+
(((x[0] + x[1] - 1) / 2, 0), (0, (1 - x[0] - x[1]) / 2)),
84+
((x[0] / 2, 0), (x[0], -x[0] / 2)),
85+
((x[1] / 2, -x[1]), (0, -x[1] / 2)),
86+
]
87+
]
88+
else:
89+
assert reference.tdim == 3
90+
functions = [
91+
MatrixFunction(i)
92+
for i in [
93+
(
94+
(2 * (1 - x[0] - x[1] - x[2]) / 3, 0, 0),
95+
(0, (x[0] + x[1] + x[2] - 1) / 3, 0),
96+
(0, 0, (x[0] + x[1] + x[2] - 1) / 3),
97+
),
98+
(
99+
((x[0] + x[1] + x[2] - 1) / 3, 0, 0),
100+
(0, 2 * (1 - x[0] - x[1] - x[2]) / 3, 0),
101+
(0, 0, (x[0] + x[1] + x[2] - 1) / 3),
102+
),
103+
((x[0] / 3, 0, 0), (x[0], -2 * x[0] / 3, 0), (0, 0, x[0] / 3)),
104+
((x[0] / 3, 0, 0), (0, x[0] / 3, 0), (x[0], 0, -2 * x[0] / 3)),
105+
((-x[1] / 3, 0, 0), (0, -x[1] / 3, 0), (0, -x[1], 2 * x[1] / 3)),
106+
((-x[1] / 3, x[1], 0), (0, 2 * x[1] / 3, 0), (0, x[1], -x[1] / 3)),
107+
((x[2] / 3, 0, -x[2]), (0, x[2] / 3, -x[2]), (0, 0, -2 * x[2] / 3)),
108+
((-2 * x[2] / 3, 0, x[2]), (0, x[2] / 3, 0), (0, 0, x[2] / 3)),
109+
]
110+
]
111+
112+
lagrange = Lagrange(reference, order - 1)
113+
for dof, f in zip(lagrange.dofs, lagrange.get_basis_functions()):
114+
for g in functions:
115+
dofs.append(
116+
IntegralMoment(
117+
reference,
118+
f * g,
119+
dof,
120+
(reference.tdim, 0),
121+
"co_contravariant",
122+
)
123+
)
124+
125+
super().__init__(
126+
reference,
127+
order,
128+
poly,
129+
dofs,
130+
reference.tdim,
131+
reference.tdim**2,
132+
(reference.tdim, reference.tdim),
133+
)
134+
135+
def init_kwargs(self) -> typing.Dict[str, typing.Any]:
136+
"""Return the kwargs used to create this element.
137+
138+
Returns:
139+
Keyword argument dictionary
140+
"""
141+
return {}
142+
143+
@property
144+
def lagrange_subdegree(self) -> int:
145+
return self.order
146+
147+
@property
148+
def lagrange_superdegree(self) -> typing.Optional[int]:
149+
return self.order
150+
151+
@property
152+
def polynomial_subdegree(self) -> int:
153+
return self.order
154+
155+
@property
156+
def polynomial_superdegree(self) -> typing.Optional[int]:
157+
return self.order
158+
159+
names = ["Lederer-Schoberl"]
160+
references = ["triangle", "tetrahedron"]
161+
min_order = 0
162+
continuity = "inner H(curl div)"
163+
value_type = "matrix"
164+
last_updated = "2024.03"

symfem/finite_element.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,13 @@ def get_piece(f, point):
521521
assert dim == 2
522522
f = [f[1, 1], f[2, 2]]
523523
g = [g[1, 1], g[2, 2]]
524+
elif continuity == "inner H(curl div)":
525+
if len(vertices[0]) == 2:
526+
f = f[1, 0]
527+
g = g[1, 0]
528+
if len(vertices[0]) == 3:
529+
f = [f[1, 0], f[2, 0]]
530+
g = [g[1, 0], g[2, 0]]
524531
elif continuity == "integral inner H(div)":
525532
f = f[0, 0].integrate((x[1], 0, 1))
526533
g = g[0, 0].integrate((x[1], 0, 1))

symfem/functionals.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"DivergenceIntegralMoment",
4040
"TangentIntegralMoment",
4141
"NormalIntegralMoment",
42+
"TraceIntegralMoment",
4243
"NormalDerivativeIntegralMoment",
4344
"InnerProductIntegralMoment",
4445
"NormalInnerProductIntegralMoment",
@@ -1638,6 +1639,68 @@ def get_tex(self) -> typing.Tuple[str, typing.List[str]]:
16381639
name = "Normal integral moment"
16391640

16401641

1642+
class TraceIntegralMoment(IntegralMoment):
1643+
"""An integral moment of the trace of a matrix."""
1644+
1645+
def __init__(
1646+
self,
1647+
reference: Reference,
1648+
f_in: FunctionInput,
1649+
dof: BaseFunctional,
1650+
entity: typing.Tuple[int, int],
1651+
mapping: typing.Union[str, None] = "contravariant",
1652+
):
1653+
"""Create the functional.
1654+
1655+
Args:
1656+
reference: The reference cell
1657+
f_in: The function to multiply with
1658+
dof: The DOF in a moment space that the function is associated with
1659+
entity: The entity the functional is associated with
1660+
mapping: The type of mappping from the reference cell to a physical cell
1661+
"""
1662+
scalar_f = parse_function_input(f_in).subs(x, t)
1663+
assert scalar_f.is_scalar
1664+
super().__init__(
1665+
reference,
1666+
scalar_f,
1667+
dof,
1668+
entity=entity,
1669+
mapping=mapping,
1670+
map_function=False,
1671+
)
1672+
1673+
def dot(self, function: Function) -> ScalarFunction:
1674+
"""Take the product of a function with the trace of the matrix.
1675+
1676+
Args:
1677+
function: The function
1678+
1679+
Returns:
1680+
The inner product of the function and the matrix's trace
1681+
"""
1682+
assert function.is_matrix and function.shape[0] == function.shape[1]
1683+
trace = sum(function[i, i] for i in range(function.shape[0]))
1684+
return trace * self.f * self.integral_domain.jacobian()
1685+
1686+
def get_tex(self) -> typing.Tuple[str, typing.List[str]]:
1687+
"""Get a representation of the functional as TeX, and list of terms involved.
1688+
1689+
Returns:
1690+
Representation of the functional as TeX, and list of terms involved
1691+
"""
1692+
entity = self.entity_tex()
1693+
entity_def = self.entity_definition()
1694+
desc = "\\boldsymbol{V}\\mapsto"
1695+
desc += f"\\displaystyle\\int_{{{entity}}}"
1696+
desc += "\\operatorname{tr}(\\boldsymbol{V})"
1697+
if self.f != 1:
1698+
desc += "(" + _to_tex(self.f, True) + ")"
1699+
return desc, [entity_def]
1700+
1701+
name = "Trace integral moment"
1702+
1703+
16411704
class NormalDerivativeIntegralMoment(DerivativeIntegralMoment):
16421705
"""An integral moment in the normal direction."""
16431706

0 commit comments

Comments
 (0)