Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3035 +/- ##
==========================================
+ Coverage 84.12% 84.24% +0.11%
==========================================
Files 258 258
Lines 56449 56854 +405
Branches 4817 4845 +28
==========================================
+ Hits 47487 47894 +407
+ Misses 8138 8136 -2
Partials 824 824
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
| obj._transpose = kwargs.get("transpose", direct) | ||
| obj._method = kwargs.get("method", 'FD') | ||
| obj._weights = cls._process_weights(**kwargs) | ||
| obj._halo = cls._validate_halo(kwargs.get("halo")) |
There was a problem hiding this comment.
would "support" be a better word?
There was a problem hiding this comment.
I don't think so, since the "support region" conventionally refers to the footprint of the stencil itself, whilst this is for what happens at the edge of the domain
| 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
| from itertools import product | ||
|
|
||
| import numpy as np | ||
| import sympy |
There was a problem hiding this comment.
as sp? Might as well remove the import below and use sympy.prod regardless
| 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
?
| the argument as zero outside the equation's SubDomain) are supported. | ||
| """ | ||
| if halo not in (None, 0): | ||
| raise ValueError(f"Expected halo=None or halo=0, got halo={halo}") |
There was a problem hiding this comment.
looks like this should be a boolean, not None or 0
There was a problem hiding this comment.
It could but 0 is a better representation of what it does, which is to treat the halo as zeros.
There was a problem hiding this comment.
it's just that u can't do not halo .. 😬 but OK
| 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
|
|
||
| def __new__(cls, *args, halo_radius=None, **kwargs): | ||
| obj = super().__new__(cls, *args, **kwargs) | ||
| # With `halo=0`, the stencil radius (see `Differentiable.halo_radius`) |
There was a problem hiding this comment.
sorry for being pedantic, but imho this now wants a docstring. halo is undefined in this class scope, and having both halo and halo_radius doesn't help
| 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
| if self._interp_mode is not None: | ||
| kwargs['interp_mode'] = self._interp_mode | ||
|
|
||
| # Derivatives with `halo=0` treat their argument as zero outside the |
There was a problem hiding this comment.
this comment ain't useful here
|
|
||
| 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?
…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 (halo_radius), 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.
d581d3f to
6573324
Compare
No description provided.