Skip to content

Commit 6552d82

Browse files
committed
api: fix injection witjh staggering
1 parent 7562312 commit 6552d82

3 files changed

Lines changed: 122 additions & 70 deletions

File tree

devito/operations/interpolators.py

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def r(self):
230230
return self.sfunction.r
231231

232232
@memoized_meth
233-
def _weights(self, subdomain=None):
233+
def _weights(self, subdomain=None, shifts=None):
234234
raise NotImplementedError
235235

236236
@property
@@ -243,8 +243,24 @@ def _cdim(self):
243243
dims = [self.sfunction._crdim(d) for d in self._gdims]
244244
return dims
245245

246+
def _field_shifts(self, field):
247+
"""
248+
Per-grid-Dimension half-cell shift induced by ``field``'s staggering
249+
(e.g. ``h_x/2`` for a field staggered in ``x``). Returns None for
250+
unstaggered fields. SubDomain-induced origin offsets are deliberately
251+
ignored — they are not staggering.
252+
"""
253+
try:
254+
staggered = field.staggered
255+
except AttributeError:
256+
return None
257+
if not staggered or all(s == 0 for s in staggered):
258+
return None
259+
return tuple((d.spacing / 2) if s else 0
260+
for d, s in zip(self._gdims, staggered, strict=True))
261+
246262
@memoized_meth
247-
def _rdim(self, subdomain=None):
263+
def _rdim(self, subdomain=None, shifts=None):
248264
# If the interpolation operation is limited to a SubDomain,
249265
# use the SubDimensions of that SubDomain
250266
if subdomain:
@@ -254,7 +270,7 @@ def _rdim(self, subdomain=None):
254270

255271
# Make radius dimension conditional to avoid OOB
256272
rdims = []
257-
pos = self.sfunction._position_map.values()
273+
pos = self.sfunction._position_map(shifts=shifts).values()
258274

259275
for (d, rd, p) in zip(gdims, self._cdim, pos, strict=True):
260276
# Add conditional to avoid OOB
@@ -300,27 +316,34 @@ def _augment_implicit_dims(self, implicit_dims, extras=None):
300316
idims = extra + as_tuple(implicit_dims) + self.sfunction.dimensions
301317
return tuple(idims)
302318

303-
def _coeff_temps(self, implicit_dims):
319+
def _coeff_temps(self, implicit_dims, shifts=None):
304320
return []
305321

306-
def _positions(self, implicit_dims):
322+
def _positions(self, implicit_dims, shifts=None):
307323
return [Eq(v, INT(floor(k)), implicit_dims=implicit_dims)
308-
for k, v in self.sfunction._position_map.items()]
324+
for k, v in self.sfunction._position_map(shifts=shifts).items()]
309325

310-
def _interp_idx(self, variables, implicit_dims=None, subdomain=None):
326+
def _interp_idx(self, variables, implicit_dims=None, subdomain=None,
327+
shifts=None):
311328
"""
312329
Generate interpolation indices for the DiscreteFunctions in ``variables``.
330+
331+
``shifts`` is a per-Dimension physical offset for the target field's
332+
origin: it only affects the integer position symbol via the position
333+
map (``pos = floor((c - o - shift)/h)``). The index substitution itself
334+
is unchanged — any staggered offset in a field's own symbolic access is
335+
absorbed by Devito's normal indexing.
313336
"""
314-
pos = self.sfunction._position_map.values()
337+
pos = self.sfunction._position_map(shifts=shifts).values()
315338

316339
# Temporaries for the position
317-
temps = self._positions(implicit_dims)
340+
temps = self._positions(implicit_dims, shifts=shifts)
318341

319342
# Coefficient symbol expression
320-
temps.extend(self._coeff_temps(implicit_dims))
343+
temps.extend(self._coeff_temps(implicit_dims, shifts=shifts))
321344

322345
# Substitution mapper for variables
323-
mapper = self._rdim(subdomain=subdomain).getters
346+
mapper = self._rdim(subdomain=subdomain, shifts=shifts).getters
324347

325348
# Index substitution to make in variables
326349
subs = {
@@ -451,9 +474,11 @@ def _inject(self, field, expr, implicit_dims=None):
451474

452475
subdomain = _extract_subdomain(fields)
453476

454-
# Derivatives must be evaluated before the introduction of indirect accesses
477+
# Derivatives must be evaluated before the introduction of indirect
478+
# accesses. Variables are sampled at their own grid location; the
479+
# position map for the target field carries the staggering so the
480+
# field's stencil neighbors land on the right indices.
455481
try:
456-
# Expr must be evaluated ath the field's location.
457482
_exprs = tuple(e._eval_at(f).evaluate
458483
for e, f in zip(exprs, fields, strict=True))
459484
except AttributeError:
@@ -464,22 +489,29 @@ def _inject(self, field, expr, implicit_dims=None):
464489

465490
# Implicit dimensions
466491
implicit_dims = self._augment_implicit_dims(implicit_dims, variables)
492+
493+
# All fields in a single injection share the same staggering by
494+
# construction (they are written together at the same indices), so
495+
# derive shifts from the first field.
496+
shifts = self._field_shifts(fields[0])
497+
467498
# Move all temporaries inside inner loop to improve parallelism
468-
# Can only be done for inject as interpolation need a temporary
469-
# summing temp that wouldn't allow collapsing
499+
# Can only be done for inject as interpolation needs a summing temp
500+
# that wouldn't allow collapsing
470501
implicit_dims = implicit_dims + tuple(r.parent for r in
471-
self._rdim(subdomain=subdomain))
502+
self._rdim(subdomain=subdomain,
503+
shifts=shifts))
472504

473505
# List of indirection indices for all adjacent grid points
474-
finterp = fields + as_tuple(variables)
475-
idx_subs, temps = self._interp_idx(finterp, implicit_dims=implicit_dims,
476-
subdomain=subdomain)
506+
idx_subs, temps = self._interp_idx(list(fields) + variables,
507+
implicit_dims=implicit_dims,
508+
subdomain=subdomain, shifts=shifts)
477509

478-
# Substitute coordinate base symbols into the interpolation coefficients
479510
eqns = [Inc(_field.xreplace(idx_subs),
480-
(self._weights(subdomain=subdomain) * _expr).xreplace(idx_subs),
511+
(self._weights(subdomain=subdomain, shifts=shifts)
512+
* _expr).xreplace(idx_subs),
481513
implicit_dims=implicit_dims)
482-
for (_field, _expr) in zip(fields, _exprs, strict=True)]
514+
for _field, _expr in zip(fields, _exprs, strict=True)]
483515

484516
return temps + eqns
485517

@@ -497,24 +529,27 @@ class LinearInterpolator(WeightedInterpolator):
497529
_name = 'linear'
498530

499531
@memoized_meth
500-
def _weights(self, subdomain=None):
501-
rdim = self._rdim(subdomain=subdomain)
532+
def _weights(self, subdomain=None, shifts=None):
533+
rdim = self._rdim(subdomain=subdomain, shifts=shifts)
502534
c = [(1 - p) * (1 - r) + p * r
503-
for (p, d, r) in zip(self._point_symbols, self._gdims, rdim, strict=True)]
535+
for (p, d, r) in zip(self._point_symbols(shifts), self._gdims, rdim,
536+
strict=True)]
504537
return Mul(*c)
505538

506-
@cached_property
507-
def _point_symbols(self):
539+
@memoized_meth
540+
def _point_symbols(self, shifts=None):
508541
"""Symbol for coordinate value in each Dimension of the point."""
509542
dtype = self.sfunction.coordinates.dtype
510-
return DimensionTuple(*(Symbol(name=f'p{d}', dtype=dtype)
543+
suffix = self.sfunction._shifts_suffix(shifts)
544+
return DimensionTuple(*(Symbol(name=f'p{d}{suffix}', dtype=dtype)
511545
for d in self.grid.dimensions),
512546
getters=self.grid.dimensions)
513547

514-
def _coeff_temps(self, implicit_dims):
548+
def _coeff_temps(self, implicit_dims, shifts=None):
515549
# Positions
516-
pmap = self.sfunction._position_map
517-
poseq = [Eq(self._point_symbols[d], pos - floor(pos),
550+
pmap = self.sfunction._position_map(shifts=shifts)
551+
psyms = self._point_symbols(shifts)
552+
poseq = [Eq(psyms[d], pos - floor(pos),
518553
implicit_dims=implicit_dims)
519554
for (d, pos) in zip(self._gdims, pmap.keys(), strict=True)]
520555
return poseq
@@ -533,23 +568,24 @@ class PrecomputedInterpolator(WeightedInterpolator):
533568

534569
_name = 'precomp'
535570

536-
def _positions(self, implicit_dims):
571+
def _positions(self, implicit_dims, shifts=None):
537572
if self.sfunction.gridpoints_data is None:
538-
return super()._positions(implicit_dims)
573+
return super()._positions(implicit_dims, shifts=shifts)
539574
else:
540575
# No position temp as we have directly the gridpoints
541576
return[Eq(p, k, implicit_dims=implicit_dims)
542-
for (k, p) in self.sfunction._position_map.items()]
577+
for (k, p) in self.sfunction._position_map(shifts=shifts).items()]
543578

544579
@property
545580
def interpolation_coeffs(self):
546581
return self.sfunction.interpolation_coeffs
547582

548583
@memoized_meth
549-
def _weights(self, subdomain=None):
584+
def _weights(self, subdomain=None, shifts=None):
550585
ddim, cdim = self.interpolation_coeffs.dimensions[1:]
551586
mappers = [{ddim: ri, cdim: rd-rd.parent.symbolic_min}
552-
for (ri, rd) in enumerate(self._rdim(subdomain=subdomain))]
587+
for (ri, rd) in enumerate(self._rdim(subdomain=subdomain,
588+
shifts=shifts))]
553589
return Mul(*[self.interpolation_coeffs.subs(mapper)
554590
for mapper in mappers])
555591

@@ -594,8 +630,8 @@ def interpolation_coeffs(self):
594630
return tuple(coeffs)
595631

596632
@memoized_meth
597-
def _weights(self, subdomain=None):
598-
rdims = self._rdim(subdomain=subdomain)
633+
def _weights(self, subdomain=None, shifts=None):
634+
rdims = self._rdim(subdomain=subdomain, shifts=shifts)
599635
return Mul(*[
600636
w._subs(rd, rd-rd.parent.symbolic_min)
601637
for (rd, w) in zip(rdims, self.interpolation_coeffs, strict=True)

devito/types/sparse.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,18 @@ def coordinates_data(self):
366366
except AttributeError:
367367
return None
368368

369-
@cached_property
370-
def _pos_symbols(self):
371-
return [Symbol(name=f'pos{d}', dtype=np.int32)
369+
@memoized_meth
370+
def _pos_symbols(self, shifts=None):
371+
suffix = self._shifts_suffix(shifts)
372+
return [Symbol(name=f'pos{d}{suffix}', dtype=np.int32)
372373
for d in self.grid.dimensions]
373374

375+
@staticmethod
376+
def _shifts_suffix(shifts):
377+
if not shifts or all(s == 0 for s in shifts):
378+
return ''
379+
return '_s' + ''.join('1' if s != 0 else '0' for s in shifts)
380+
374381
@cached_property
375382
def _point_increments(self):
376383
"""Index increments in each Dimension for each point symbol."""
@@ -380,19 +387,25 @@ def _point_increments(self):
380387
def _point_support(self):
381388
return np.array(self._point_increments)
382389

383-
@cached_property
384-
def _position_map(self):
390+
@memoized_meth
391+
def _position_map(self, shifts=None):
385392
"""
386-
Symbols map for the physical position of the sparse points relative to the grid
387-
origin.
393+
Symbols map for the physical position of the sparse points relative to the
394+
origin of the target field. ``shifts`` is a tuple, one entry per grid
395+
Dimension, of additional physical offsets to subtract from the sparse
396+
coordinates (e.g. ``h_x/2`` for a field staggered in ``x``). If ``shifts``
397+
is None, only the grid origin is subtracted.
388398
"""
399+
if shifts is None:
400+
shifts = (0,) * len(self.grid.dimensions)
389401
return OrderedDict([
390-
((c - o)/d.spacing, p)
391-
for p, c, d, o in zip(
392-
self._pos_symbols,
402+
((c - o - s)/d.spacing, p)
403+
for p, c, d, o, s in zip(
404+
self._pos_symbols(shifts=shifts),
393405
self._coordinate_symbols,
394406
self.grid.dimensions,
395407
self.grid.origin_symbols,
408+
shifts,
396409
strict=True
397410
)
398411
])
@@ -419,7 +432,6 @@ def _cond_rdim(self, dim, cond):
419432
parent = self._crdim(dim)
420433
return ConditionalDimension(parent.name, parent, condition=cond, indirect=True)
421434

422-
423435
def _eval_at(self, func):
424436
return self
425437

@@ -451,7 +463,7 @@ def guard(self, expr=None):
451463
conditions = {}
452464

453465
# Position map and temporaries for it
454-
pmap = self._position_map
466+
pmap = self._position_map()
455467

456468
# Temporaries for the position
457469
temps = self.interpolator._positions(self.dimensions)
@@ -713,9 +725,6 @@ def _dist_scatter(self, alias=None, data=None):
713725
mapper.update(self._dist_subfunc_scatter(sf))
714726
return mapper
715727

716-
def _eval_at(self, func):
717-
return self
718-
719728
def _halo_exchange(self):
720729
# no-op for SparseFunctions
721730
return
@@ -1286,8 +1295,8 @@ def _coordinate_symbols(self):
12861295
return tuple([self.coordinates._subs(d_dim, i)
12871296
for i in range(self.grid.dim)])
12881297

1289-
@cached_property
1290-
def _position_map(self):
1298+
@memoized_meth
1299+
def _position_map(self, shifts=None):
12911300
"""
12921301
Symbol for each grid index according to the coordinates.
12931302
@@ -1307,12 +1316,12 @@ def _position_map(self):
13071316
(self.gridpoints._subs(ddim, di), p)
13081317
for (di, p) in zip(
13091318
range(self.grid.dim),
1310-
self._pos_symbols,
1319+
self._pos_symbols(shifts=shifts),
13111320
strict=True
13121321
)
13131322
)
13141323
else:
1315-
return super()._position_map
1324+
return super()._position_map(shifts=shifts)
13161325

13171326

13181327
class PrecomputedSparseTimeFunction(AbstractSparseTimeFunction,

tests/test_interpolation.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from conftest import assert_structure
88
from devito import (
9-
DefaultDimension, Dimension, Eq, Function, Grid, MatrixSparseTimeFunction, NODE,
9+
NODE, DefaultDimension, Dimension, Eq, Function, Grid, MatrixSparseTimeFunction,
1010
Operator, PrecomputedSparseFunction, PrecomputedSparseTimeFunction, Real,
1111
SparseFunction, SparseTimeFunction, SubDomain, TimeFunction, switchconfig
1212
)
@@ -485,21 +485,28 @@ def test_inject_staggered(self, stagg):
485485
op = Operator(expr)
486486

487487
op()
488-
# The expected value is 1 for the NODE case.
489-
# For the staggered case, the center point is
490-
# shifted left compared to the staggered function
491488
if stagg == 'NODE':
492489
assert np.isclose(a.data[5, 5, 5], 2, rtol=1e-6)
493490
# all other should be zero
494491
assert np.sum(a.data) == 2
495-
elif len(as_tuple(staggered)) == 1:
496-
from IPython import embed; embed()
497-
#
498-
assert np.isclose(a.data[5, 5, 5], 1, rtol=1e-6)
499-
elif len(as_tuple(staggered)) == 2:
500-
expected = 0.25
501-
elif len(as_tuple(staggered)) == 3:
502-
expected = 0.125
492+
else:
493+
# Bottom corner since the source position is left of the staggered field
494+
corner = [5, 5, 5] - np.array(a.staggered)
495+
# Indices touched by the interpolation based on staggering
496+
slices = [slice(corner[i], 6) for i in range(3)]
497+
# Number of points for the interpolation.
498+
# Single dim only interpolates between two points,
499+
# so 2**(number of staggered dims) is the number of points.
500+
npoints = 2**(np.sum(np.array(a.staggered, dtype=np.int32)))
501+
# b value at the staggered field from b._eval_at(field)
502+
b_val = (1 * (npoints - 1) + 2) / npoints
503+
# Then source interpolation. Source at the center of the staggered field,
504+
# so all points have the same weight 1/npoints.
505+
interp_val = b_val / npoints
506+
assert np.allclose(a.data[slices], interp_val, rtol=1e-6)
507+
# All other should be zero so should sum to the interp_val * number of points.
508+
# Use abs to make sure there is no +- cancellations
509+
assert np.sum(np.abs(a.data)) == interp_val * 2**(sum(np.array(a.staggered)))
503510

504511
# ---------------------------------------------------------------------------
505512
# Precomputed interpolation / injection
@@ -824,7 +831,7 @@ def test_point_symbol_types(self, dtype, expected):
824831
grid = Grid(shape=(11,))
825832
s = SparseFunction(name='src', npoint=1,
826833
grid=grid, dtype=dtype)
827-
point_symbol = s.interpolator._point_symbols[0]
834+
point_symbol = s.interpolator._point_symbols()[0]
828835

829836
assert point_symbol.dtype is expected
830837

0 commit comments

Comments
 (0)