Skip to content

Commit 4a593ff

Browse files
Gradient recovery by L2-projection (#13)
* Start recovery module * Add unit tests for recover_gradient_l2 --------- Co-authored-by: Stephan Kramer <s.kramer@imperial.ac.uk>
1 parent 85f8d46 commit 4a593ff

3 files changed

Lines changed: 173 additions & 0 deletions

File tree

adapt_common/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from adapt_common.norms import * # noqa
2+
from adapt_common.recovery import * # noqa
23
from adapt_common.reduction import * # noqa
34
from adapt_common.utility import * # noqa

adapt_common/recovery.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Module containing functions for recovering derivatives of Functions."""
2+
3+
import firedrake as fd
4+
import ufl
5+
from firedrake.petsc import PETSc
6+
7+
__all__ = ["recover_gradient_l2"]
8+
9+
10+
@PETSc.Log.EventDecorator()
11+
def recover_gradient_l2(f, target_space=None):
12+
r"""Recover the gradient of a scalar or vector field using :math:`L^2` projection.
13+
14+
:arg f: the scalar field whose derivatives we seek to recover
15+
:type f: :class:`firedrake.function.Function`
16+
:kwarg mesh: the underlying mesh
17+
:type mesh: :class:`firedrake.mesh.MeshGeometry`
18+
:kwarg target_space: the vector-valued function space to recover the gradient in
19+
:type target_space: :class:`firedrake.functionspaceimpl.FunctionSpace`
20+
:returns: recovered gradient
21+
:rtype: :class:`firedrake.function.Function`
22+
"""
23+
if target_space is None:
24+
if not isinstance(f, fd.Function):
25+
val_err = (
26+
"If a target space is not provided then the input must be a Function."
27+
)
28+
raise ValueError(val_err)
29+
source_degree = f.ufl_element().degree()
30+
if source_degree <= 0:
31+
val_err = "Input Function must be at least degree 1."
32+
raise ValueError(val_err)
33+
target_degree = source_degree - 1
34+
mesh = f.function_space().mesh()
35+
rank = len(f.function_space().value_shape)
36+
family = "DG" if target_degree == 0 else "CG"
37+
if rank == 0:
38+
target_space = fd.VectorFunctionSpace(mesh, family, target_degree)
39+
elif rank == 1:
40+
target_space = fd.TensorFunctionSpace(
41+
mesh,
42+
family,
43+
target_degree,
44+
shape=(f.function_space().value_size, mesh.geometric_dimension),
45+
)
46+
else:
47+
val_err = (
48+
"L2 projection can only be used to compute gradients of scalar or"
49+
f" vector Functions, not Functions of rank {rank}."
50+
)
51+
raise ValueError(val_err)
52+
53+
return fd.project(ufl.grad(f), target_space)

test/test_recovery_l2.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Unit tests for derivative recovery based on L2-projection."""
2+
3+
import firedrake as fd
4+
import pytest
5+
6+
from adapt_common.recovery import recover_gradient_l2
7+
8+
9+
@pytest.fixture(params=[1, 2, 3])
10+
def dim(request):
11+
"""Set the spatial dimension."""
12+
return request.param
13+
14+
15+
@pytest.fixture
16+
def mesh(dim):
17+
"""Create a uniform simplex mesh."""
18+
n = 4
19+
return {
20+
1: fd.UnitIntervalMesh(n),
21+
2: fd.UnitSquareMesh(n, n),
22+
3: fd.UnitCubeMesh(n, n, n),
23+
}[dim]
24+
25+
26+
def test_recover_gradient_p2_scalar(mesh):
27+
"""Test gradient recovery for a P2 scalar field."""
28+
# Define a scalar function in P2 space
29+
f = fd.Function(fd.FunctionSpace(mesh, "CG", 2))
30+
x = fd.SpatialCoordinate(mesh)
31+
f.interpolate(sum([0.5 * xi**2 for xi in x]))
32+
33+
# Recovery its gradient using L2 projection
34+
grad_f = recover_gradient_l2(f)
35+
36+
# Check the function space of the recovered gradient
37+
element = grad_f.function_space().ufl_element()
38+
assert element.family() == "Lagrange"
39+
assert element.degree() == 1
40+
assert element.num_sub_elements == mesh.geometric_dimension
41+
42+
# Verify the accuracy of the recovered gradient
43+
expected = x
44+
assert fd.errornorm(expected, grad_f, norm_type="L2") == pytest.approx(0, abs=1e-8)
45+
46+
47+
def test_recover_gradient_p2_vector(mesh):
48+
"""Test gradient recovery for a P2 vector field."""
49+
# Define a vector function in P2 space
50+
f = fd.Function(fd.VectorFunctionSpace(mesh, "CG", 2))
51+
x = fd.SpatialCoordinate(mesh)
52+
f.interpolate(fd.as_vector([0.5 * xi**2 for xi in fd.SpatialCoordinate(mesh)]))
53+
54+
# Recovery its gradient using L2 projection
55+
grad_f = recover_gradient_l2(f)
56+
57+
# Check the function space of the recovered gradient
58+
element = grad_f.function_space().ufl_element()
59+
assert element.family() == "Lagrange"
60+
assert element.degree() == 1
61+
assert element.num_sub_elements == mesh.geometric_dimension**2
62+
63+
# Verify the accuracy of the recovered gradient
64+
expected = fd.Function(fd.TensorFunctionSpace(mesh, "CG", 1))
65+
expected.interpolate(
66+
fd.as_tensor(
67+
[
68+
[xi if i == j else 0 for j in range(mesh.geometric_dimension)]
69+
for i, xi in enumerate(x)
70+
]
71+
)
72+
)
73+
assert fd.errornorm(expected, grad_f, norm_type="L2") == pytest.approx(0, abs=1e-8)
74+
75+
76+
def test_recover_gradient_p1_scalar(mesh):
77+
"""Test gradient recovery for a P1 scalar field."""
78+
# Define a scalar function in P1 space
79+
f = fd.Function(fd.FunctionSpace(mesh, "CG", 1))
80+
x = fd.SpatialCoordinate(mesh)
81+
f.interpolate(sum(x))
82+
83+
# Recovery its gradient using L2 projection
84+
grad_f = recover_gradient_l2(f)
85+
86+
# Check the function space of the recovered gradient
87+
element = grad_f.function_space().ufl_element()
88+
assert element.family() == "Discontinuous Lagrange"
89+
assert element.degree() == 0
90+
assert element.num_sub_elements == mesh.geometric_dimension
91+
92+
# Verify the accuracy of the recovered gradient
93+
expected = fd.Function(grad_f.function_space()).assign(1.0)
94+
assert fd.errornorm(expected, grad_f, norm_type="L2") == pytest.approx(0, abs=1e-8)
95+
96+
97+
def test_recover_gradient_invalid_input():
98+
"""Test that an error is raised for invalid input."""
99+
val_err = "If a target space is not provided then the input must be a Function."
100+
with pytest.raises(ValueError, match=val_err):
101+
recover_gradient_l2("not_a_function")
102+
103+
104+
def test_recover_gradient_degree_error(mesh):
105+
"""Test that an error is raised for degree below 1."""
106+
val_err = "Input Function must be at least degree 1."
107+
with pytest.raises(ValueError, match=val_err):
108+
recover_gradient_l2(fd.Function(fd.FunctionSpace(mesh, "DG", 0)))
109+
110+
111+
def test_recover_gradient_rank_error(mesh):
112+
"""Test that an error is raised for unsupported function ranks."""
113+
f = fd.Function(fd.TensorFunctionSpace(mesh, "CG", 2))
114+
val_err = (
115+
"L2 projection can only be used to compute gradients of scalar or vector"
116+
" Functions, not Functions of rank 2."
117+
)
118+
with pytest.raises(ValueError, match=val_err):
119+
recover_gradient_l2(f)

0 commit comments

Comments
 (0)