Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3035 +/- ##
==========================================
+ Coverage 84.13% 84.25% +0.12%
==========================================
Files 258 258
Lines 56513 56918 +405
Branches 4825 4853 +28
==========================================
+ Hits 47547 47958 +411
+ Misses 8140 8135 -5
+ Partials 826 825 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
3ae0cf3 to
a56e2c6
Compare
0707079 to
384bb84
Compare
| return self._subs(dim, dim + shift) | ||
| expr = self | ||
| for d in self.free_symbols: | ||
| if d is dim or not getattr(d, 'is_Space', False): |
There was a problem hiding this comment.
isn't it safer to check for if not isinstance(d, Dimensions): continue, then u don't need getattr?
in fact, u may also use retrieve_dimensions or something like that instead of free_symbols
| # A SubDistributor wraps an MPI communicator, which can't and shouldn't be pickled | ||
| state.pop('_distributor', None) | ||
| # Memoized grown SubDomains and masks are rebuilt on demand | ||
| state.pop('_memoized_meth__cache_meth', None) |
There was a problem hiding this comment.
this looks fairly hacky
| Max(0, Min(1, upper - point + 1))) | ||
|
|
||
|
|
||
| class GrownSubDomain(SubDomain): |
There was a problem hiding this comment.
I don't see the need for this subclass
| radius : frozendict of {Dimension: int} | ||
| Number of points to grow by, per root Dimension. | ||
| """ | ||
| return GrownSubDomain(self, radius) |
There was a problem hiding this comment.
why do you need a subclass?
There was a problem hiding this comment.
I agree - couldn't you just have a parent attribute on SubDomain or similar?
| # E.g., extract `1/h_x` | ||
| rule = lambda e: e.is_Pow and (not e.exp.is_Number or e.exp < 0) | ||
| rule = lambda e: (e.is_Pow and (not e.exp.is_Number or e.exp < 0) and | ||
| not bare_stencil_dimensions(e)) |
| pass | ||
| # Aliases are built by translating Indexeds; a term reading an | ||
| # iteration Dimension outside of any Indexed cannot be translated | ||
| if any(not d.is_Stencil for d in bare_dimensions(a)): |
There was a problem hiding this comment.
this is a correctness check , and so it doesn't belong here (see below...)
| return maybe_coeff, others | ||
|
|
||
|
|
||
| def bare_dimensions(expr): |
There was a problem hiding this comment.
this thing you did with bare_dimensions is quite fishy, why do you need it ?
if the problem are unbound StencilDimensions , then you could use https://github.com/devitocodes/devito/blob/main/devito/ir/support/utils.py#L267
and the right place to discard alias candidates for correctness reasons is def _do_generate probably around line 290, when u see that ... & exclude
a check looking for unbound dimensions may have to be added there, and there only
| and e.halo is not None)) | ||
|
|
||
| @staticmethod | ||
| def _extend(rhs, subdomain): |
There was a problem hiding this comment.
imho this doesn't belong here but should be part of the _eval_at machinery, where subdomain is now also available
rationale : we may want to be able to evaluate any expressions , not just Eq's, restricting to a subdomain
There was a problem hiding this comment.
Seconded - the imports inside functions also implies that these are misplaced imo
| # E.g. `x + i*h_x` into `f(x)` s.t. `f(x + i*h_x)` | ||
| expr = expr._subs(dim, indices.expr) | ||
| expr = expr.shift(dim, indices.expr - dim) | ||
| if restricted: |
There was a problem hiding this comment.
potentially nitpicking: you could systematically pass a SubDomain , the whole grid if u don't have a SubDomain, with the grid's indicator being 1 everywhere; in that case, you would substantially reduce the special-casing
| """ | ||
| return sympify((index - self.dim) / self.spacing) | ||
|
|
||
| @property |
| rule = lambda e: e.is_Function or (e.is_Pow and e.exp.is_Number and 0 < e.exp < 1) | ||
| rule = lambda e: ((e.is_Function or | ||
| (e.is_Pow and e.exp.is_Number and 0 < e.exp < 1)) and | ||
| not bare_stencil_dimensions(e)) |
There was a problem hiding this comment.
Readability is also becoming an issue here - a function would be better than a lambda. That being said, probably irrelevant given @FabioLuporini 's comments
| """True if the rhs has derivatives with halo=0.""" | ||
| from devito.symbolics import search # noqa | ||
|
|
||
| return bool(search(self.rhs, lambda e: getattr(e, 'is_Derivative', False) |
| and e.halo is not None)) | ||
|
|
||
| @staticmethod | ||
| def _extend(rhs, subdomain): |
There was a problem hiding this comment.
Seconded - the imports inside functions also implies that these are misplaced imo
| # The terms without indicators have no halo=0 derivative, so they are | ||
| # restricted to the SubDomain at the evaluation point | ||
| here = sympy.Mul(*[subdomain.indicator(d, 0, r) for d, r in radius.items()]) | ||
| terms = [t if search(t, SubDomainIndicator) else here * t |
There was a problem hiding this comment.
search returns a set by default - is ordering determinism a problem here? Perhaps we should add a mode to search (ordered-unique) that returns an OrderedSet if so, since I'm pretty sure it will come in use
| radius : frozendict of {Dimension: int} | ||
| Number of points to grow by, per root Dimension. | ||
| """ | ||
| return GrownSubDomain(self, radius) |
There was a problem hiding this comment.
I agree - couldn't you just have a parent attribute on SubDomain or similar?
| """ | ||
| return GrownSubDomain(self, radius) | ||
|
|
||
| def indicator(self, dim, offset, radius): |
There was a problem hiding this comment.
Should this be a constructor class method? That being said, if this is the only way SubDomainIndicators are going to get constructed, then why not fold this into the __init__
| def radius(self): | ||
| return int(self.args[4]) | ||
|
|
||
| @property |
| def define(self, dimensions): | ||
| sizes = dict(zip(dimensions, self.subdomain.grid.shape, strict=True)) | ||
| grown = {} | ||
| for d, v in self.subdomain.define(dimensions).items(): | ||
| radius = self.radius.get(d, 0) | ||
| if isinstance(v, Dimension) or radius == 0: | ||
| grown[d] = v | ||
| elif v[0] == 'middle': | ||
| ltkn, rtkn = max(v[1] - radius, 0), max(v[2] - radius, 0) | ||
| grown[d] = d if ltkn == rtkn == 0 else ('middle', ltkn, rtkn) | ||
| else: | ||
| side, thickness = v | ||
| grown[d] = (side, min(thickness + radius, sizes[d])) | ||
| return grown |
There was a problem hiding this comment.
No need to go through define here - just construct the SubDimensions directly as needed. Probably irrelevant in the light of other comments however
1682f1c to
b02b62e
Compare
| @property | ||
| def subdomain(self): | ||
| """ | ||
| With halo=0, the SubDomain outside of which the argument is treated as |
There was a problem hiding this comment.
just to be sure, do you actually need both halo and subdomain? or is subdomain just enough (essentially , when NOT None, it encodes the fact the users wants 0-valued taps outside of it) by any chance?
There was a problem hiding this comment.
This only gets set at evaluate when it neeeds one. So conceptually it could but it would be very intricated and would need a lot of weird logic to use only one
There was a problem hiding this comment.
I don't understand why that would be the case?
There was a problem hiding this comment.
Because the subdomain cannot be constructed without knowing the radius
| def _halo_radius(self): | ||
| """ | ||
| Distance, per root Dimension, by which the evaluated derivatives with | ||
| halo=0 in the expression extend it past the SubDomain their argument is |
There was a problem hiding this comment.
a bit contrived explanation but maybe it's just me... could use an example
|
|
||
| @cached_property | ||
| def _has_halo(self): | ||
| """True if the expression has derivatives with halo=0.""" |
|
|
||
| expr = self | ||
| for d in retrieve_dimensions(self, mode='unique'): | ||
| if d is not dim and d.root is dim.root and (d.is_Sub or d is dim.root): |
There was a problem hiding this comment.
can u not simplify this with _defines somehow
There was a problem hiding this comment.
_defines also contains ConditionalDimensions (xc._defines == {xc, x}), which must not be shifted, so the explicit check stays.
There was a problem hiding this comment.
so to be sure the following would be borked:
if d in dim._defines and not d.is_NonlinearDerived
?
There was a problem hiding this comment.
Not quite but tweaked
| is_commutative = True | ||
|
|
||
| __rkwargs__ = ('base',) | ||
| __rkwargs__ = ('base', '_halo_radius') |
There was a problem hiding this comment.
no prefix _
btw, u mean halo_map ?
since the need for this is essentially SubDomains, I'm not sure calling "halo-something" is actually informative, halo is such a generic and overloaded term... but again, it might just be me
There was a problem hiding this comment.
It's not padding no, and halo is what is used throughout to define/deominate this part of a stencil.
| The unbound StencilDimensions of `expr` appearing outside of any Indexed, | ||
| which translating `expr` into an alias would not shift. | ||
| """ | ||
| return unbounded(expr) & set(retrieve_dimensions(expr, mode='unique')) |
There was a problem hiding this comment.
what's the & set(retrieve_dimensions(expr, mode='unique')) part for? isn't unbounded(expr) already a subset of retrieve_dimensions(expr) ?
There was a problem hiding this comment.
Also retrieve_dimensions(expr, mode='unique') is already a set
There was a problem hiding this comment.
isn't unbounded(expr) already a subset of retrieve_dimensions(expr)
No unbounded only look for IndexDerivative
| try: | ||
| lhs = self.lhs._evaluate(**kwargs) | ||
| rhs = self.rhs._eval_at(self.lhs, **kwargs)._evaluate(**kwargs) | ||
| rhs = self.rhs._eval_at(self.lhs, **at, **kwargs)._evaluate(**kwargs) |
There was a problem hiding this comment.
I don't think u need this **at thing , in fact u don't need at at all -- just pass ..., subdomain=self.subdomain, **kwargs)....
| return eq | ||
|
|
||
| @cached_property | ||
| def _has_halo(self): |
There was a problem hiding this comment.
you also have _halo_halo in differentiable.py; if the recursion is done properly, you shouldn't need this method -- you can likely drop it; if you can't because something breaks, it's likely there's a deeper issue
There was a problem hiding this comment.
anyway, I still think we can find a better alternative to "has_halo" , since it doesn't capture the SubDomain meaning, and halo is such an overloaded word
There was a problem hiding this comment.
This is an Eq it's not differentiable
| eq = self.func(lhs, rhs, subdomain=self.subdomain, | ||
|
|
||
| # ... and extend the rhs past it by the radius of their stencils | ||
| if restrict: |
There was a problem hiding this comment.
probably just if self._has_halo
| @property | ||
| def subdomain(self): | ||
| """ | ||
| With halo=0, the SubDomain outside of which the argument is treated as |
| is_commutative = True | ||
|
|
||
| __rkwargs__ = ('base',) | ||
| __rkwargs__ = ('base', '_halo_radius') |
| The unbound StencilDimensions of `expr` appearing outside of any Indexed, | ||
| which translating `expr` into an alias would not shift. | ||
| """ | ||
| return unbounded(expr) & set(retrieve_dimensions(expr, mode='unique')) |
There was a problem hiding this comment.
Also retrieve_dimensions(expr, mode='unique') is already a set
| if self.parent is None: | ||
| return self.define(grid.dimensions) | ||
|
|
||
| sizes = dict(zip(grid.dimensions, grid.shape, strict=True)) |
There was a problem hiding this comment.
grid.shape is already a DimensionTuple, so no need for this mapper
| Branch-free indicator of the point `offset` points away from the current | ||
| point along `dim` lying within this SubDomain: 1 inside, 0 outside. |
There was a problem hiding this comment.
This docstring is a little difficult to parse
| offset : expr-like | ||
| The offset, e.g. 2, or `i0` for a stencil in unexpanded form. | ||
| """ | ||
| sub = self.dimension_map.get(dim, dim) |
| for d in self.dimensions: | ||
| if d.is_Sub: | ||
| expr = expr * self.indicator(d.root, 0) |
There was a problem hiding this comment.
Is it worth having this product as a cached property? Since self.indicator = 1 when the dimension is not a SubDimension, is the special-casing needed?
There was a problem hiding this comment.
Cache what product, this applies the indicator to the input expr, it's not a property.
| super().__init__(**kwargs) | ||
|
|
||
| def define(self, dimensions): | ||
| return {dimensions[0]: ('left', self.width)} |
There was a problem hiding this comment.
Does this not need to do anything for the other dimensions?
d2f12f7 to
d581d3f
Compare
| does not look within Indexeds, so the intersection with `unbounded(expr)` | ||
| leaves out those appearing only in Indexeds, which are translated. | ||
| """ | ||
| return unbounded(expr) & retrieve_dimensions(expr, mode='unique') |
There was a problem hiding this comment.
I'm not entirely sure if you're actually just trying to working around a bug inside unbounded
There was a problem hiding this comment.
Not a bug in unbounded: collect builds the pivot by rewriting Indexeds only, so an i0 used outside them (e.g. in the halo=0 indicator) is not shifted with them; untranslatable leaves such terms out of the alias.
|
|
||
| terms = cbk_compose(i) | ||
|
|
||
| # Aliases only translate Indexeds, so a term reading an unbound |
There was a problem hiding this comment.
can u give me an example of one such term?
There was a problem hiding this comment.
oh... but then this is not a DDA issue, this is more of an unsupported use case, because theoretically u could have hoisted that MAX(...) expression too -- it's just that it doesn't work right now... though for IndexDerivative it might well be working? I think this might be failing for OSS devito when we pre-evaluate everything.
78114ff to
edc087f
Compare
…pressions A derivative of an expression mixing Grid Functions, e.g. f(x), with Functions defined on a SubDomain, e.g. p(ix), only substituted the derivative Dimension in the stencil. Functions indexed by the sibling Dimension stayed unshifted, so (f + p).dx silently dropped p and (f*p).dx never shifted p. Differentiable.shift now shifts every space Dimension sharing the root of the given Dimension, and make_derivative builds the stencil points with it, in both the expanded and unexpanded paths. Tests cover Add and Mul with both expansion modes, and a one-face CPML forward/adjoint dot test whose adjoint term mixes Grid and SubDomain Functions inside .dx.T.
CIRE builds aliases by translating Indexeds. An unbound StencilDimension appearing outside of any Indexed, e.g. `i0` in `x + i0` within the body of an IndexDerivative, is not translated, so the alias would be evaluated at the wrong stencil point. For example, the invariants pass hoisted such an expression into an array over `x`, dropping `i0`. `_do_generate` now leaves the terms reading such StencilDimensions out of the alias, or discards the candidate altogether.
…djoints `expr.dx(halo=0)` treats `expr` as zero outside the SubDomain of its equation, and `.T` keeps the flag. Hence `Inc(q_bar, out_bar.dx(halo=0).T, subdomain=S)` is the adjoint of `Eq(out, q.dx, subdomain=S)`, i.e. D^T R^T with R the restriction to S, without a user-side zero-padded work field. Equations without `halo=0` derivatives, or without a SubDomain, are evaluated as before. - An equation evaluates its rhs with `_eval_at(lhs, subdomain=S)`. A `halo=0` derivative records S, and multiplies each stencil tap by a branch-free integer MIN/MAX indicator of the grid point it reads being in S (SubDomain.indicator), which invariant hoisting computes once. Along the Dimensions it does not differentiate, it restricts its argument to S at the evaluation point. In a sum with such derivatives, the other terms are restricted to S at the evaluation point (SubDomain.restrict); factors are not. - The evaluated derivative records its stencil radius as its extent past S (Differentiable.extent), and the equation iterates over S grown by the largest one (SubDomain.grow, a SubDomain with a parent). Tensor equations aggregate their components. - It works for expanded, unexpanded and staggered derivatives. Derivatives of `halo=0` derivatives, MultiSubDomains and methods other than FD raise NotImplementedError. Tests: dot tests of `Eq(out, g.dx, subdomain=S)` against `Inc(g_bar, out_bar.dx(halo=0).T, subdomain=S)` for left/right/middle SubDomains, first and second derivatives, orders 2/4/8, Eq and Inc, 2D and unexpanded forms, in 1D and 3D; staggered dot tests against `-out_bar.dx(halo=0)`, as in elastic adjoints, including a staggered vector equation; sums, factors and vector equations; SubDomain growth; a forward check; and a one-face CPML operator.
edc087f to
b48a7e6
Compare
| return any(a._has_zero_halo for a in self._args_diff) | ||
|
|
||
| @cached_property | ||
| def extent(self): |
There was a problem hiding this comment.
IIRC, I think "extent" is for physical quantities ? also, is it possible this should be a private property? I doubt the use needs to be able to query it
|
|
||
| terms = cbk_compose(i) | ||
|
|
||
| # Aliases only translate Indexeds, so a term reading an unbound |
There was a problem hiding this comment.
oh... but then this is not a DDA issue, this is more of an unsupported use case, because theoretically u could have hoisted that MAX(...) expression too -- it's just that it doesn't work right now... though for IndexDerivative it might well be working? I think this might be failing for OSS devito when we pre-evaluate everything.
No description provided.