Skip to content

api: Fix mixed subdomain derivative - #3035

Open
mloubout wants to merge 3 commits into
mainfrom
fix-mixed-subdomain-derivative
Open

mloubout wants to merge 3 commits into
mainfrom
fix-mixed-subdomain-derivative

Conversation

@mloubout

Copy link
Copy Markdown
Contributor

No description provided.

@mloubout mloubout added API api (symbolics, types, ...) feature-request labels Sep 28, 2026
@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.29907% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.24%. Comparing base (64d15c5) to head (6573324).

Files with missing lines Patch % Lines
devito/passes/clusters/aliases.py 75.00% 1 Missing and 1 partial ⚠️
devito/types/grid.py 97.67% 1 Missing ⚠️
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              
Flag Coverage Δ
pytest-gpu-aomp-amdgpuX 68.61% <58.10%> (-0.06%) ⬇️
pytest-gpu-gcc- 78.99% <99.29%> (+0.15%) ⬆️
pytest-gpu-icx- 78.93% <99.29%> (+0.17%) ⬆️
pytest-gpu-nvc-nvidiaX 69.14% <58.10%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mloubout
mloubout force-pushed the fix-mixed-subdomain-derivative branch 2 times, most recently from 3ae0cf3 to a56e2c6 Compare September 29, 2026 01:11
@mloubout
mloubout marked this pull request as ready for review September 29, 2026 01:37
@mloubout
mloubout force-pushed the fix-mixed-subdomain-derivative branch 2 times, most recently from 0707079 to 384bb84 Compare September 29, 2026 03:51
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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would "support" be a better word?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread devito/types/grid.py Outdated
# 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks fairly hacky

Comment thread devito/types/grid.py Outdated
Max(0, Min(1, upper - point + 1)))


class GrownSubDomain(SubDomain):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the need for this subclass

Comment thread devito/types/grid.py Outdated
radius : frozendict of {Dimension: int}
Number of points to grow by, per root Dimension.
"""
return GrownSubDomain(self, radius)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need a subclass?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - couldn't you just have a parent attribute on SubDomain or similar?

Comment thread devito/passes/clusters/aliases.py Outdated
# 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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as before

Comment thread devito/passes/clusters/aliases.py Outdated
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)):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a correctness check , and so it doesn't belong here (see below...)

Comment thread devito/passes/clusters/aliases.py Outdated
return maybe_coeff, others


def bare_dimensions(expr):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread devito/types/equation.py Outdated
and e.halo is not None))

@staticmethod
def _extend(rhs, subdomain):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread devito/finite_differences/tools.py Outdated
"""
return sympify((index - self.dim) / self.spacing)

@property

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cached property?

Comment thread devito/passes/clusters/aliases.py Outdated
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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability is also becoming an issue here - a function would be better than a lambda. That being said, probably irrelevant given @FabioLuporini 's comments

Comment thread devito/types/equation.py Outdated
"""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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use retrieve_derivatives?

Comment thread devito/types/equation.py Outdated
and e.halo is not None))

@staticmethod
def _extend(rhs, subdomain):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seconded - the imports inside functions also implies that these are misplaced imo

Comment thread devito/types/equation.py Outdated
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread devito/types/grid.py
from itertools import product

import numpy as np
import sympy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as sp? Might as well remove the import below and use sympy.prod regardless

Comment thread devito/types/grid.py Outdated
radius : frozendict of {Dimension: int}
Number of points to grow by, per root Dimension.
"""
return GrownSubDomain(self, radius)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - couldn't you just have a parent attribute on SubDomain or similar?

Comment thread devito/types/grid.py Outdated
"""
return GrownSubDomain(self, radius)

def indicator(self, dim, offset, radius):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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__

Comment thread devito/types/grid.py Outdated
def radius(self):
return int(self.args[4])

@property

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cached?

Comment thread devito/types/grid.py Outdated
Comment on lines +825 to +838
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to go through define here - just construct the SubDimensions directly as needed. Probably irrelevant in the light of other comments however

@mloubout
mloubout force-pushed the fix-mixed-subdomain-derivative branch 9 times, most recently from 1682f1c to b02b62e Compare September 30, 2026 04:38
@property
def subdomain(self):
"""
With halo=0, the SubDomain outside of which the argument is treated as

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why that would be the case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking `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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can u not simplify this with _defines somehow

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_defines also contains ConditionalDimensions (xc._defines == {xc, x}), which must not be shifted, so the explicit check stays.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this should be a boolean, not None or 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could but 0 is a better representation of what it does, which is to treat the halo as zeros.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's just that u can't do not halo .. 😬 but OK

is_commutative = True

__rkwargs__ = ('base',)
__rkwargs__ = ('base', '_halo_radius')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subdomain_padding maybe?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not padding no, and halo is what is used throughout to define/deominate this part of a stencil.

Comment thread devito/passes/clusters/aliases.py Outdated
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'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the & set(retrieve_dimensions(expr, mode='unique')) part for? isn't unbounded(expr) already a subset of retrieve_dimensions(expr) ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also retrieve_dimensions(expr, mode='unique') is already a set

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't unbounded(expr) already a subset of retrieve_dimensions(expr)

No unbounded only look for IndexDerivative

Comment thread devito/types/equation.py Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think u need this **at thing , in fact u don't need at at all -- just pass ..., subdomain=self.subdomain, **kwargs)....

Comment thread devito/types/equation.py Outdated
return eq

@cached_property
def _has_halo(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an Eq it's not differentiable

Comment thread devito/types/equation.py Outdated
eq = self.func(lhs, rhs, subdomain=self.subdomain,

# ... and extend the rhs past it by the radius of their stencils
if restrict:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably just if self._has_halo

Comment thread devito/finite_differences/derivative.py
@property
def subdomain(self):
"""
With halo=0, the SubDomain outside of which the argument is treated as

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point

is_commutative = True

__rkwargs__ = ('base',)
__rkwargs__ = ('base', '_halo_radius')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subdomain_padding maybe?

Comment thread devito/passes/clusters/aliases.py Outdated
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'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also retrieve_dimensions(expr, mode='unique') is already a set

Comment thread devito/types/grid.py
Comment thread devito/types/grid.py Outdated
if self.parent is None:
return self.define(grid.dimensions)

sizes = dict(zip(grid.dimensions, grid.shape, strict=True))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grid.shape is already a DimensionTuple, so no need for this mapper

Comment thread devito/types/grid.py Outdated
Comment on lines +746 to +747
Branch-free indicator of the point `offset` points away from the current
point along `dim` lying within this SubDomain: 1 inside, 0 outside.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docstring is a little difficult to parse

Comment thread devito/types/grid.py Outdated
offset : expr-like
The offset, e.g. 2, or `i0` for a stencil in unexpanded form.
"""
sub = self.dimension_map.get(dim, dim)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: maybe_subdim =?

Comment thread devito/types/grid.py
Comment on lines +769 to +771
for d in self.dimensions:
if d.is_Sub:
expr = expr * self.indicator(d.root, 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache what product, this applies the indicator to the input expr, it's not a property.

Comment thread tests/test_derivatives.py Outdated
super().__init__(**kwargs)

def define(self, dimensions):
return {dimensions[0]: ('left', self.width)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this not need to do anything for the other dimensions?

@mloubout
mloubout force-pushed the fix-mixed-subdomain-derivative branch 2 times, most recently from d2f12f7 to d581d3f Compare October 1, 2026 14:10

def __new__(cls, *args, halo_radius=None, **kwargs):
obj = super().__new__(cls, *args, **kwargs)
# With `halo=0`, the stencil radius (see `Differentiable.halo_radius`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure if you're actually just trying to working around a bug inside unbounded

Comment thread devito/types/equation.py
if self._interp_mode is not None:
kwargs['interp_mode'] = self._interp_mode

# Derivatives with `halo=0` treat their argument as zero outside the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment ain't useful here


terms = cbk_compose(i)

# Aliases only translate Indexeds, so a term reading an unbound

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@mloubout
mloubout force-pushed the fix-mixed-subdomain-derivative branch from d581d3f to 6573324 Compare October 1, 2026 15:48

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API api (symbolics, types, ...) feature-request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants