@@ -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 )
0 commit comments