Skip to content

Commit a6f1b27

Browse files
authored
Backport bug fixes (#459)
* Fix split for symmetric tensors (#448) * BUG: prevent ReferenceGradient(Zero) on CoefficientSplitter (#455) * BUG: prevent ReferenceGradient(Zero) on CoefficientSplitter * test * fix for mixed dimension cells * cleanup * Pullback: remove FunctionSpace (#452) * Pullback: remove FunctionSpace * cast to int * import Zero * CoefficientSplitter: fix again ReferenceGrad(Zero) (#460)
1 parent a342682 commit a6f1b27

File tree

7 files changed

+115
-35
lines changed

7 files changed

+115
-35
lines changed

test/test_apply_coefficent_split.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
MeshSequence,
88
triangle,
99
)
10+
from ufl.algorithms import extract_coefficients
1011
from ufl.algorithms.apply_coefficient_split import apply_coefficient_split
1112
from ufl.classes import (
1213
ComponentTensor,
@@ -64,3 +65,25 @@ def test_apply_coefficient_split(self):
6465
op1_, (idx1_,) = op1.ufl_operands
6566
assert op1_ == PositiveRestricted(ReferenceGrad(ReferenceValue(f1)))
6667
assert idx1_ == idx1
68+
69+
70+
def test_derivative_zero_simplication():
71+
cell = triangle
72+
mesh = Mesh(LagrangeElement(cell, 1, (2,)))
73+
elem0 = LagrangeElement(cell, 0)
74+
elem1 = LagrangeElement(cell, 3)
75+
V0 = FunctionSpace(mesh, elem0)
76+
V1 = FunctionSpace(mesh, elem1)
77+
f0 = Coefficient(V0)
78+
f1 = Coefficient(V1)
79+
elem = MixedElement([elem0, elem1])
80+
V = FunctionSpace(mesh, elem)
81+
f = Coefficient(V)
82+
83+
coefficient_split = {f: (f0, f1)}
84+
expr = ReferenceGrad(ReferenceGrad(ReferenceValue(f)))
85+
expr_split = apply_coefficient_split(expr, coefficient_split)
86+
87+
coefficients = extract_coefficients(expr_split)
88+
assert f1 in coefficients
89+
assert f0 not in coefficients

test/test_derivative.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from itertools import chain
55

6-
from utils import FiniteElement, LagrangeElement, MixedElement
6+
from utils import FiniteElement, LagrangeElement, MixedElement, SymmetricElement
77

88
from ufl import (
99
CellDiameter,
@@ -574,6 +574,37 @@ def test_multiple_indexed_coefficient_derivative(self):
574574
assertEqualBySampling(actual, expected)
575575

576576

577+
def test_split_tensor_coefficient_derivative(self):
578+
cell = triangle
579+
dim = 2
580+
domain = Mesh(LagrangeElement(cell, 1, (dim,)))
581+
symmetry = {(0, 0): 0, (0, 1): 1, (1, 0): 1, (1, 1): 2}
582+
ncomp = (dim * (dim + 1)) // 2
583+
584+
S = SymmetricElement(symmetry, [LagrangeElement(cell, 1)] * ncomp)
585+
V = LagrangeElement(cell, 1, (dim,))
586+
587+
element = MixedElement([S, V])
588+
Z = FunctionSpace(domain, element)
589+
z = Coefficient(Z)
590+
test = TestFunction(Z)
591+
s, u = split(z)
592+
t, v = split(test)
593+
594+
J = 0.5 * inner(s, s) + inner(s, nabla_grad(u))
595+
596+
dJ_du = derivative(J, u, v)
597+
dJ_ds = derivative(J, s, t)
598+
599+
actual = dJ_ds + dJ_du
600+
601+
expected = (
602+
0.5 * inner(t, s) + 0.5 * inner(s, t) + inner(t, nabla_grad(u)) + inner(s, nabla_grad(v))
603+
)
604+
605+
assertEqualBySampling(actual, expected)
606+
607+
577608
def test_segregated_derivative_of_convection(self):
578609
cell = tetrahedron
579610
V = LagrangeElement(cell, 1)

test/test_split.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ def test_split(self):
4343

4444
# Shapes of subelements are reproduced:
4545
g = Coefficient(m_space)
46-
(s,) = g.ufl_shape
46+
(size,) = g.ufl_shape
4747
for g2 in split(g):
48-
s -= product(g2.ufl_shape)
49-
assert s == 0
48+
size -= product(g2.ufl_shape)
49+
assert size == 0
5050

5151
# Mixed elements of non-scalar subelements are flattened
5252
v2 = MixedElement([v, v])
@@ -78,3 +78,12 @@ def test_split(self):
7878
assert split(split(split(t)[0])[1]) == (t[2],)
7979
assert split(split(split(t)[1])[0]) == (t[3],)
8080
assert split(split(split(t)[1])[1]) == (t[4], t[5])
81+
82+
# Split twice on nested mixed elements with symmetry
83+
vs = MixedElement([v, s])
84+
vs_space = FunctionSpace(domain, vs)
85+
vs_test = TestFunction(vs_space)
86+
87+
v_test, s_test = split(vs_test)
88+
assert split(v_test) == (vs_test[0], vs_test[1])
89+
assert split(s_test) == (vs_test[2], vs_test[3], vs_test[4], vs_test[5])

ufl/algorithms/apply_coefficient_split.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ReferenceValue,
2121
Restricted,
2222
Terminal,
23+
Zero,
2324
)
2425
from ufl.core.multiindex import indices
2526
from ufl.corealg.dag_traverser import DAGTraverser
@@ -209,8 +210,16 @@ def _handle_terminal(
209210
c = o
210211
if reference_value:
211212
c = ReferenceValue(c)
212-
for _ in range(reference_grad):
213+
for k in range(reference_grad):
213214
c = ReferenceGrad(c)
215+
if isinstance(c, Zero):
216+
gdim = c.ufl_shape[-1]
217+
c = Zero(
218+
c.ufl_shape + (gdim,) * (reference_grad - k - 1),
219+
c.ufl_free_indices,
220+
c.ufl_index_dimensions,
221+
)
222+
break
214223
if restricted == "+":
215224
c = PositiveRestricted(c)
216225
elif restricted == "-":

ufl/formoperators.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# Modified by Massimiliano Leoni, 2016
1010
# Modified by Cecile Daversin-Catty, 2018
1111

12+
import numpy as np
13+
1214
from ufl.action import Action
1315
from ufl.adjoint import Adjoint
1416
from ufl.algorithms import (
@@ -270,7 +272,9 @@ def set_list_item(li, i, v):
270272
def _handle_derivative_arguments(form, coefficient, argument):
271273
"""Handle derivative arguments."""
272274
# Wrap single coefficient in tuple for uniform treatment below
273-
if isinstance(coefficient, list | tuple | ListTensor):
275+
if isinstance(coefficient, ListTensor):
276+
coefficients = tuple(coefficient[i] for i in np.ndindex(coefficient.ufl_shape))
277+
elif isinstance(coefficient, list | tuple):
274278
coefficients = tuple(coefficient)
275279
else:
276280
coefficients = (coefficient,)

ufl/pullback.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from ufl.core.expr import Expr
1616
from ufl.core.multiindex import indices
1717
from ufl.domain import AbstractDomain, MeshSequence, extract_unique_domain
18-
from ufl.functionspace import FunctionSpace
1918
from ufl.tensors import as_tensor
2019

2120
if TYPE_CHECKING:
@@ -428,15 +427,14 @@ def apply(self, expr, domain=None):
428427
subdomain = domain[i] if isinstance(domain, MeshSequence) else None
429428
rmapped = subelem.pullback.apply(rsub, domain=subdomain)
430429
# Flatten into the pulled back expression for the whole thing
431-
g_components.extend([rmapped[idx] for idx in np.ndindex(rmapped.ufl_shape)])
430+
g_components.extend(rmapped[idx] for idx in np.ndindex(rmapped.ufl_shape))
432431
offset += subelem.reference_value_size
433432
# And reshape appropriately
434-
space = FunctionSpace(domain, self._element)
435-
f = as_tensor(np.asarray(g_components).reshape(space.value_shape))
436-
if f.ufl_shape != space.value_shape:
433+
value_shape = self.physical_value_shape(self._element, domain)
434+
f = as_tensor(np.asarray(g_components).reshape(value_shape))
435+
if f.ufl_shape != value_shape:
437436
raise ValueError(
438-
"Expecting pulled back expression with shape "
439-
f"'{space.value_shape}', got '{f.ufl_shape}'"
437+
f"Expecting pulled back expression with shape '{value_shape}', got '{f.ufl_shape}'"
440438
)
441439
return f
442440

@@ -453,7 +451,8 @@ def physical_value_shape(self, element, domain) -> tuple[int, ...]:
453451
assert element == self._element
454452
domains = domain.iterable_like(element)
455453
dim = sum(
456-
FunctionSpace(d, e).value_size for d, e in zip(domains, self._element.sub_elements)
454+
int(np.prod(e.pullback.physical_value_shape(e, d), dtype=int))
455+
for d, e in zip(domains, self._element.sub_elements)
457456
)
458457
return (dim,)
459458

@@ -496,8 +495,6 @@ def apply(self, expr, domain=None):
496495
497496
Returns: The function pulled back to the reference cell
498497
"""
499-
domain = extract_unique_domain(expr)
500-
space = FunctionSpace(domain, self._element)
501498
rflat = [expr[idx] for idx in np.ndindex(expr.ufl_shape)]
502499
g_components = []
503500
offsets = [0]
@@ -520,13 +517,13 @@ def apply(self, expr, domain=None):
520517
subdomain = domain[i] if isinstance(domain, MeshSequence) else None
521518
rmapped = subelem.pullback.apply(rsub, domain=subdomain)
522519
# Flatten into the pulled back expression for the whole thing
523-
g_components.extend([rmapped[idx] for idx in np.ndindex(rmapped.ufl_shape)])
520+
g_components.extend(rmapped[idx] for idx in np.ndindex(rmapped.ufl_shape))
524521
# And reshape appropriately
525-
f = as_tensor(np.asarray(g_components).reshape(space.value_shape))
526-
if f.ufl_shape != space.value_shape:
522+
value_shape = self.physical_value_shape(self._element, domain)
523+
f = as_tensor(np.asarray(g_components).reshape(value_shape))
524+
if f.ufl_shape != value_shape:
527525
raise ValueError(
528-
f"Expecting pulled back expression with shape "
529-
f"'{space.value_shape}', got '{f.ufl_shape}'"
526+
f"Expecting pulled back expression with shape '{value_shape}', got '{f.ufl_shape}'"
530527
)
531528
return f
532529

@@ -543,7 +540,7 @@ def physical_value_shape(self, element, domain) -> tuple[int, ...]:
543540
assert isinstance(element, type(self._element))
544541
subelem = element.sub_elements[0]
545542
pvs = subelem.pullback.physical_value_shape(subelem, domain)
546-
return tuple(i + 1 for i in max(self._symmetry.keys())) + pvs
543+
return tuple(int(i) + 1 for i in max(self._symmetry.keys())) + pvs
547544

548545

549546
class PhysicalPullback(AbstractPullback):

ufl/split_functions.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
# SPDX-License-Identifier: LGPL-3.0-or-later
77
#
88
# Modified by Anders Logg, 2008
9+
# Modified by Pablo Brubeck, 2025
10+
11+
import numpy as np
912

1013
from ufl.domain import extract_unique_domain
1114
from ufl.functionspace import FunctionSpace
1215
from ufl.indexed import Indexed
1316
from ufl.permutation import compute_indices
14-
from ufl.tensors import ListTensor, as_matrix, as_vector
17+
from ufl.pullback import SymmetricPullback
18+
from ufl.tensors import ListTensor, as_tensor
1519
from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides
1620
from ufl.utils.sequences import product
1721

@@ -33,7 +37,8 @@ def split(v):
3337

3438
elif isinstance(v, ListTensor):
3539
# Special case: split previous output of split again
36-
ops = v.ufl_operands
40+
ops = tuple(v[i] for i in np.ndindex(v.ufl_shape))
41+
3742
if all(isinstance(comp, Indexed) for comp in ops):
3843
args = [comp.ufl_operands[0] for comp in ops]
3944
if all(args[0] == args[i] for i in range(1, len(args))):
@@ -85,8 +90,18 @@ def split(v):
8590
# Build expressions representing the subfunction of v for each subelement
8691
offset = begin
8792
sub_functions = []
88-
domains = domain.iterable_like(element)
89-
for i, (d, e) in enumerate(zip(domains, element.sub_elements)):
93+
94+
# Deal with symmetry
95+
if isinstance(element.pullback, SymmetricPullback):
96+
symmetry = element.pullback._symmetry
97+
indices = (comp for idx, comp in sorted(symmetry.items()))
98+
else:
99+
indices = range(len(element.sub_elements))
100+
101+
domains = tuple(domain.iterable_like(element))
102+
for i in indices:
103+
d = domains[i]
104+
e = element.sub_elements[i]
90105
# Get shape, size, indices, and v components
91106
# corresponding to subelement value
92107
shape = FunctionSpace(d, e).value_shape
@@ -99,16 +114,8 @@ def split(v):
99114
# Shape components into same shape as subelement
100115
if rank == 0:
101116
(subv,) = components
102-
elif rank <= 1:
103-
subv = as_vector(components)
104-
elif rank == 2:
105-
subv = as_matrix(
106-
[components[i * shape[1] : (i + 1) * shape[1]] for i in range(shape[0])]
107-
)
108117
else:
109-
raise ValueError(
110-
f"Don't know how to split functions with sub functions of rank {rank}."
111-
)
118+
subv = as_tensor(np.reshape(components, shape))
112119

113120
offset += sub_size
114121
sub_functions.append(subv)

0 commit comments

Comments
 (0)