@@ -102,7 +102,7 @@ def _fd_priority(self):
102102
103103 __rargs__ = ('expr' , '*dims' )
104104 __rkwargs__ = ('side' , 'deriv_order' , 'fd_order' , 'transpose' , '_ppsubs' ,
105- 'x0' , 'method' , 'weights' )
105+ 'x0' , 'method' , 'weights' , 'halo' , 'subdomain' )
106106
107107 def __new__ (cls , expr , * dims , ** kwargs ):
108108 # Validate the input arguments `expr`, `dims` and `deriv_order`
@@ -161,6 +161,8 @@ def __new__(cls, expr, *dims, **kwargs):
161161 obj ._transpose = kwargs .get ("transpose" , direct )
162162 obj ._method = kwargs .get ("method" , 'FD' )
163163 obj ._weights = cls ._process_weights (** kwargs )
164+ obj ._halo = cls ._validate_halo (kwargs .get ("halo" ))
165+ obj ._subdomain = kwargs .get ("subdomain" )
164166
165167 ppsubs = kwargs .get ("subs" , kwargs .get ("_ppsubs" , []))
166168 processed = []
@@ -177,6 +179,16 @@ def __new__(cls, expr, *dims, **kwargs):
177179
178180 return obj
179181
182+ @staticmethod
183+ def _validate_halo (halo ):
184+ """
185+ Validate `halo`. Only None (read the argument everywhere) and 0 (treat
186+ the argument as zero outside the equation's SubDomain) are supported.
187+ """
188+ if halo not in (None , 0 ):
189+ raise ValueError (f"Expected halo=None or halo=0, got halo={ halo } " )
190+ return halo
191+
180192 @staticmethod
181193 def _validate_expr (expr ):
182194 """
@@ -325,6 +337,8 @@ def _process_weights(cls, **kwargs):
325337 def __call__ (self , x0 = None , fd_order = None , side = None , method = None , ** kwargs ):
326338 weights = kwargs .get ('weights' , kwargs .get ('w' ))
327339 rkw = {}
340+ if 'halo' in kwargs :
341+ rkw ['halo' ] = kwargs ['halo' ]
328342 if side is not None :
329343 rkw ['side' ] = side
330344 if method is not None :
@@ -457,6 +471,26 @@ def side(self):
457471 def transpose (self ):
458472 return self ._transpose
459473
474+ @property
475+ def halo (self ):
476+ """
477+ None if the argument is read everywhere, 0 if it is treated as zero
478+ outside the SubDomain of the equation the Derivative belongs to.
479+ """
480+ return self ._halo
481+
482+ @property
483+ def subdomain (self ):
484+ """
485+ With halo=0, the SubDomain outside of which the argument is treated as
486+ zero, set upon evaluation within an equation restricted to it.
487+ """
488+ return self ._subdomain
489+
490+ @cached_property
491+ def _has_zero_halo (self ):
492+ return self .halo is not None or self .expr ._has_zero_halo
493+
460494 @property
461495 def is_TimeDependent (self ):
462496 return self .expr .is_TimeDependent
@@ -481,26 +515,21 @@ def T(self):
481515
482516 return self ._rebuild (transpose = adjoint )
483517
484- def _eval_at (self , func , interp_mode = 'direct' , ** kwargs ):
518+ def _eval_at (self , func , interp_mode = 'direct' , subdomain = None , ** kwargs ):
485519 """
486520 Evaluates the derivative at the location of `func`. It is necessary for staggered
487521 setup where one could have Eq(u(x + h_x/2), v(x).dx)) in which case v(x).dx
488522 has to be computed at x=x + h_x/2.
523+
524+ With halo=0, the argument is treated as zero outside `subdomain`, which
525+ the Derivative records.
489526 """
490- # No staggering, don't waste time
491- if not self .expr .staggered and not func .staggered :
492- return self
493- # If an x0 already exists or evaluating at the same function (i.e u = u.dx)
494- # do not overwrite it
495- if self .x0 or self .side is not None or func .function is self .expr .function :
496- return self
497- # For basic equation of the form f = Derivative(g, ...) we can just
498- # compare staggering
499- if self .expr .staggered == func .staggered and self .expr .is_Function :
500- return self
501- # Time derivatives are not affected by space staggering
502- if all (d .is_Time for d in self .dims ):
503- return self
527+ rkw = {}
528+ if subdomain is not None and self .halo is not None :
529+ rkw ['subdomain' ] = subdomain
530+
531+ if not self ._is_relocated (func ):
532+ return self ._rebuild (** rkw ) if rkw else self
504533
505534 # Check if x0's keys come from a DerivedDimension
506535 x0 = func .indices_ref .getters
@@ -519,7 +548,7 @@ def _eval_at(self, func, interp_mode='direct', **kwargs):
519548 # e.g f.dx(x0={x: x + h_x/2}).subs({x: ix})
520549 psubs [sd ] = d
521550 nx0 [sd ] = nx0 .pop (d )._subs (d , sd )
522- rkw = { 'x0' : nx0 }
551+ rkw [ 'x0' ] = nx0
523552 if psubs :
524553 rkw ['subs' ] = (psubs ,)
525554
@@ -534,7 +563,8 @@ def _eval_at(self, func, interp_mode='direct', **kwargs):
534563 return self ._rebuild (self .expr , ** rkw )
535564 args = [self .expr .func (* v ) for v in mapper .values ()]
536565 args .extend ([a for a in self .expr .args if a not in self .expr ._args_diff ])
537- args = [self ._rebuild (a )._eval_at (func , interp_mode = interp_mode , ** kwargs )
566+ args = [self ._rebuild (a )._eval_at (func , interp_mode = interp_mode ,
567+ subdomain = subdomain , ** kwargs )
538568 for a in args ]
539569 return self .expr .func (* args )
540570 elif self .expr .is_Mul :
@@ -549,6 +579,25 @@ def _eval_at(self, func, interp_mode='direct', **kwargs):
549579 # the expression as is.
550580 return self ._rebuild (self .expr , ** rkw )
551581
582+ def _is_relocated (self , func ):
583+ """
584+ True if the Derivative must be evaluated at the location of `func`, e.g.
585+ with `func` and the argument staggered apart.
586+ """
587+ # No staggering, don't waste time
588+ if not self .expr .staggered and not func .staggered :
589+ return False
590+ # If an x0 already exists or evaluating at the same function (i.e u = u.dx)
591+ # do not overwrite it
592+ if self .x0 or self .side is not None or func .function is self .expr .function :
593+ return False
594+ # For basic equation of the form f = Derivative(g, ...) we can just
595+ # compare staggering
596+ if self .expr .staggered == func .staggered and self .expr .is_Function :
597+ return False
598+ # Time derivatives are not affected by space staggering
599+ return not all (d .is_Time for d in self .dims )
600+
552601 def _evaluate (self , ** kwargs ):
553602 # Evaluate finite-difference.
554603 # NOTE: `evaluate` and `_eval_fd` split for potential future different
@@ -581,6 +630,13 @@ def _eval_fd(self, expr, **kwargs):
581630 if expr .is_Add and any (len (indices_at (expr , d )) > 1 for d in self .dims ):
582631 return expr .func (* [self ._eval_fd (a , ** kwargs ) for a in expr .args ])
583632
633+ # The SubDomain mask read by a halo=0 derivative can't be shifted again
634+ if expr ._has_zero_halo :
635+ raise NotImplementedError (
636+ f"{ self } differentiates a derivative with halo=0, which is not "
637+ "supported"
638+ )
639+
584640 # Step 1: Evaluate non-derivative x0. We currently enforce a simple 2nd order
585641 # interpolation to avoid very expensive finite differences on top of it
586642 x0_deriv = self ._filter_dims (self .x0 )
@@ -598,6 +654,15 @@ def _eval_fd(self, expr, **kwargs):
598654 # otherwise an IndexSum will returned
599655 expand = kwargs .get ('expand' , True )
600656
657+ # With halo=0, `expr` is treated as zero outside the equation's SubDomain
658+ subdomain = self .subdomain
659+ if subdomain is not None and (subdomain .is_MultiSubDomain or
660+ self .method != 'FD' ):
661+ raise NotImplementedError (
662+ f"halo=0 is only supported with method='FD' on a SubDomain, not "
663+ f"with method={ self .method } on { subdomain } "
664+ )
665+
601666 # Step 3: Evaluate FD of the new expression
602667 if self .method == 'RSFD' :
603668 assert len (self .dims ) == 1
@@ -607,18 +672,29 @@ def _eval_fd(self, expr, **kwargs):
607672 assert self .method == 'FD'
608673 res = cross_derivative (expr , self .dims , self .fd_order , self .deriv_order ,
609674 matvec = self .transpose , x0 = x0_deriv , expand = expand ,
610- side = self .side , weights = self .weights )
675+ side = self .side , weights = self .weights ,
676+ subdomain = subdomain )
611677 else :
612678 assert self .method == 'FD'
613679 res = generic_derivative (expr , self .dims [0 ], self .fd_order [0 ],
614680 self .deriv_order [0 ], weights = self .weights ,
615681 side = self .side , matvec = self .transpose ,
616- x0 = self .x0 , expand = expand )
682+ x0 = self .x0 , expand = expand ,
683+ subdomain = subdomain )
617684
618685 # Step 4: Apply substitutions
619686 for e in self ._ppsubs :
620687 res = res .xreplace (e )
621688
689+ # With `halo=0`, the stencil taps above are zeroed outside the SubDomain
690+ # along `self.dims` only. Along any other Dimension, the argument is read
691+ # at the evaluation point, which other `halo=0` derivatives may push
692+ # outside the SubDomain. E.g., in `f.dx(halo=0) + f.dy(halo=0)`, the
693+ # equation iterates over the SubDomain grown along both `x` and `y`, so
694+ # `f.dx` must also be zeroed where `y` lies outside the SubDomain
695+ if subdomain is not None :
696+ res = subdomain ._restrict (res , exclude = {d .root for d in self .dims })
697+
622698 return res
623699
624700 def _eval_expand_nest (self , ** hints ):
0 commit comments