Skip to content

Commit 8e805ab

Browse files
committed
dsl: Differentiate a mixed-staggering sum term by term
`Add` reports its first argument's `indices_ref`, so a sum whose terms sit at different staggered locations names a position only one of them has, and `x0` gets resolved against it for all of them. The shear strain `v_x.dy + v_y.dx` of a staggered velocity is the canonical case: both terms land on the cell corner, so a shift onto it should be a no-op, and instead each picked up a spurious one. Differentiation is linear at every order, so split such a sum in `Derivative._eval_fd`. Relative error on `D(a+b)` against `D(a) + D(b)` was 0.63 at order 0, 1.20 at order 1 and 0.95 at order 2, with `expand=False` at order 2 returning exactly zero. `generic_derivative` also short-circuited a zeroth order derivative only when `x0` was empty, building a stencil around an expression already sitting at `x0`. `index_at` answers where an expression sits, and both call sites use it.
1 parent 598061a commit 8e805ab

3 files changed

Lines changed: 73 additions & 3 deletions

File tree

devito/finite_differences/derivative.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from devito.warnings import warn
1515

1616
from .differentiable import Add, Differentiable, Mul, diffify, interp_for_fd
17-
from .finite_difference import cross_derivative, generic_derivative
17+
from .finite_difference import cross_derivative, generic_derivative, indices_at
1818
from .rsfd import d45
1919
from .tools import direct, transpose
2020

@@ -575,6 +575,12 @@ def _eval_fd(self, expr, **kwargs):
575575
shited derivative.
576576
- 4: Apply substitutions.
577577
"""
578+
# Differentiation is linear, and a sum of terms at different staggered
579+
# locations must use it: `Add` reports its first argument's location,
580+
# so `x0` would shift the other terms off the point they sat at.
581+
if expr.is_Add and any(len(indices_at(expr, d)) > 1 for d in self.dims):
582+
return expr.func(*[self._eval_fd(a, **kwargs) for a in expr.args])
583+
578584
# Step 1: Evaluate non-derivative x0. We currently enforce a simple 2nd order
579585
# interpolation to avoid very expensive finite differences on top of it
580586
x0_deriv = self._filter_dims(self.x0)

devito/finite_differences/finite_difference.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,29 @@ def cross_derivative(expr, dims, fd_order, deriv_order, x0=None, side=None, **kw
100100
return expr
101101

102102

103+
def indices_at(expr, dim):
104+
"""
105+
The locations `expr`'s terms sit at along `dim`.
106+
107+
Terms with no location of their own, a scalar say, contribute none.
108+
"""
109+
indices = set()
110+
for i in (expr.args if expr.is_Add else (expr,)):
111+
try:
112+
indices.add(i.indices_ref[dim])
113+
except (AttributeError, KeyError, IndexError, TypeError):
114+
continue
115+
return indices
116+
117+
118+
def index_at(expr, dim):
119+
"""
120+
Where `expr` sits along `dim`, or None if it does not say.
121+
"""
122+
indices = indices_at(expr, dim)
123+
return indices.pop() if len(indices) == 1 else None
124+
125+
103126
@check_input
104127
def generic_derivative(expr, dim, fd_order, deriv_order, matvec=direct, x0=None,
105128
coefficients='taylor', expand=True, weights=None, side=None):
@@ -139,8 +162,9 @@ def generic_derivative(expr, dim, fd_order, deriv_order, matvec=direct, x0=None,
139162
if deriv_order == 1 and fd_order == 2 and side is None:
140163
fd_order = 1
141164

142-
# Zeroth order derivative is just the expression itself if not shifted
143-
if deriv_order == 0 and not x0:
165+
# Zeroth order is the identity when `expr` already sits at `x0`, not a
166+
# stencil centred there.
167+
if deriv_order == 0 and (not x0 or index_at(expr, dim) == x0.get(dim)):
144168
return expr
145169

146170
# Enforce stable time coefficients

tests/test_derivatives.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,3 +1461,43 @@ def test_unevaluated(self):
14611461
assert Derivative(self.x, self.t)
14621462
assert Derivative(self.x, self.y, self.t)
14631463
assert Derivative(self.x, (self.x, 0))
1464+
1465+
1466+
@pytest.mark.parametrize('expand', [True, False])
1467+
@pytest.mark.parametrize('deriv_order', [0, 1, 2])
1468+
def test_deriv_sum_mixed_staggering(expand, deriv_order):
1469+
"""
1470+
A shifted derivative is linear: `D(a + b) == D(a) + D(b)`, at every order.
1471+
1472+
Broke for terms at different staggered locations, `Add` reporting only its
1473+
first argument's.
1474+
"""
1475+
so = 8
1476+
grid = Grid(shape=(41, 41), extent=(40., 40.))
1477+
x, y = grid.dimensions
1478+
1479+
vx = Function(name='vx', grid=grid, space_order=so, staggered=x)
1480+
vy = Function(name='vy', grid=grid, space_order=so, staggered=y)
1481+
out = Function(name='out', grid=grid, space_order=so, staggered=(x, y))
1482+
1483+
rng = np.random.default_rng(3)
1484+
for f in (vx, vy):
1485+
f.data[:] = rng.normal(size=f.shape)
1486+
1487+
def shifted(expr):
1488+
return expr.diff(y, deriv_order=deriv_order, fd_order=2,
1489+
x0={y: y + y.spacing/2})
1490+
1491+
def run(expr):
1492+
out.data[:] = 0.
1493+
Operator(Eq(out, expr), opt=('advanced', {'expand': expand})).apply()
1494+
return np.array(out.data)
1495+
1496+
s = slice(so + 3, -(so + 3))
1497+
together = run(shifted(vx.dy + vy.dx))[s, s]
1498+
apart = (run(shifted(vx.dy)) + run(shifted(vy.dx)))[s, s]
1499+
1500+
assert np.linalg.norm(apart) > 0
1501+
# float32 reassociation only: the two forms sum the same terms in a
1502+
# different order
1503+
assert np.linalg.norm(together - apart) / np.linalg.norm(apart) < 1e-5

0 commit comments

Comments
 (0)