From effb773af5d64dd258989517ab214b4348715ba0 Mon Sep 17 00:00:00 2001 From: Gerard Gorman Date: Fri, 16 Oct 2026 10:49:24 +0000 Subject: [PATCH 1/3] compiler: Make GuardFactorNe an inequality GuardFactorNe inherited CondEq through GuardFactor ahead of CondNe, so it was an equality and printed `==`. Negating a subsampling guard thus reproduced the guard itself, selecting the timesteps on the subsampled grid instead of those off it --- devito/ir/support/guards.py | 9 ++++++--- tests/test_dimension.py | 23 +++++++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/devito/ir/support/guards.py b/devito/ir/support/guards.py index 8a3842ddfd..909a5e59e1 100644 --- a/devito/ir/support/guards.py +++ b/devito/ir/support/guards.py @@ -55,7 +55,7 @@ def negated(self): # *** GuardFactor -class GuardFactor(Guard, CondEq, Pickable): +class GuardFactor(Guard): """ A guard for factor-based ConditionalDimensions. @@ -86,11 +86,14 @@ def _args_rebuild(self): return (self.d,) -class GuardFactorEq(GuardFactor, CondEq): +# The relational bases are mixed in by the leaves only, or `CondEq` would +# precede `CondNe` in the MRO of `GuardFactorNe`, making it an equality. E.g., +# `Not(GuardFactorEq(d))` would then be printed as `time % k == 0` +class GuardFactorEq(GuardFactor, CondEq, Pickable): pass -class GuardFactorNe(GuardFactor, CondNe): +class GuardFactorNe(GuardFactor, CondNe, Pickable): pass diff --git a/tests/test_dimension.py b/tests/test_dimension.py index 3e28210fe5..c869e8d02f 100644 --- a/tests/test_dimension.py +++ b/tests/test_dimension.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from sympy import And, Or +from sympy import And, Not, Or from conftest import assert_blocking, assert_structure, opts_tiling, skipif from devito import ( # noqa @@ -13,7 +13,7 @@ configuration, dimensions, floor, norm, sin, sum, switchconfig ) from devito.exceptions import InvalidArgument -from devito.ir import SymbolRegistry +from devito.ir import GuardFactor, SymbolRegistry from devito.ir.equations.algorithms import concretize_subdims from devito.ir.iet import ( Conditional, Expression, FindNodes, FindSymbols, Iteration, retrieve_iteration_tree @@ -2129,6 +2129,25 @@ def test_blocking_w_guard(self): 't,x0_blk0,y0_blk0,x,y,z,z') + def test_negated_factor_condition(self): + """ + Negating a subsampling guard must select the timesteps off the + subsampled grid. + """ + grid = Grid(shape=(4, 4)) + time = grid.time_dim + + ct = ConditionalDimension(name='ct', parent=time, factor=2) + cn = ConditionalDimension(name='cn', parent=time, + condition=Not(GuardFactor(ct)), relation='strict') + + f = TimeFunction(name='f', grid=grid, time_order=0, save=4) + + op = Operator(Eq(f, time + 1, implicit_dims=cn), name='NegatedFactor') + op.apply(time_M=3, ctf=2) + + assert np.all(f.data[:, 1, 1] == [0, 2, 0, 4]) + class TestCustomDimension: def test_shifted_minmax(self): From 90dc7c1c21c382719ed65f1d9b96133eb671cd47 Mon Sep 17 00:00:00 2001 From: Gerard Gorman Date: Thu, 15 Oct 2026 02:04:19 +0000 Subject: [PATCH 2/3] compiler: Print SymPy true/false as 1/0 in C A condition SymPy folds to `false`, e.g. `time < 0`, was printed as `if (false)`, which is not C99 without `stdbool.h` and failed to compile --- devito/passes/iet/languages/C.py | 7 +++++++ tests/test_dimension.py | 23 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/devito/passes/iet/languages/C.py b/devito/passes/iet/languages/C.py index c5ac413ac4..0f687280ac 100644 --- a/devito/passes/iet/languages/C.py +++ b/devito/passes/iet/languages/C.py @@ -89,6 +89,13 @@ class CPrinter(BasePrinter, C99CodePrinter): def _print_ImaginaryUnit(self, expr): return '_Complex_I' + # C99 has no `true`/`false` without `stdbool.h`, which is never included + def _print_BooleanTrue(self, expr): + return '1' + + def _print_BooleanFalse(self, expr): + return '0' + def _print_ListInitializer(self, expr): li = super()._print_ListInitializer(expr) if expr.dtype: diff --git a/tests/test_dimension.py b/tests/test_dimension.py index c869e8d02f..a58301abff 100644 --- a/tests/test_dimension.py +++ b/tests/test_dimension.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from sympy import And, Not, Or +from sympy import And, Not, Or, false from conftest import assert_blocking, assert_structure, opts_tiling, skipif from devito import ( # noqa @@ -2128,6 +2128,26 @@ def test_blocking_w_guard(self): 't,x0_blk0,y0_blk0,x,y,z'], 't,x0_blk0,y0_blk0,x,y,z,z') + def test_false_condition(self): + """ + A condition that SymPy folds to `false` (`time` is nonnegative) must + still be printed as valid C. + """ + grid = Grid(shape=(4, 4)) + time = grid.time_dim + + condition = time < 0 + assert condition is false + ct = ConditionalDimension(name='ct', parent=time, condition=condition) + + T = TimeFunction(name='T', grid=grid, time_order=0) + f = TimeFunction(name='f', grid=grid, time_order=0, save=2) + + op = Operator([Eq(f, T + 1, implicit_dims=ct), Eq(T, T + 1)], + name='FalseCondition') + op.apply(time_M=1) + + assert np.all(f.data[:, 1, 1] == [0, 0]) def test_negated_factor_condition(self): """ @@ -2148,6 +2168,7 @@ def test_negated_factor_condition(self): assert np.all(f.data[:, 1, 1] == [0, 2, 0, 4]) + class TestCustomDimension: def test_shifted_minmax(self): From 38e0aa407246eb9dfb536a603ee1267637ef6e41 Mon Sep 17 00:00:00 2001 From: Gerard Gorman Date: Thu, 15 Oct 2026 02:04:36 +0000 Subject: [PATCH 3/3] compiler: Drop cluster guards folded to true A condition such as `time >= 0` folds to `true` (`time` is nonnegative) and gave a Cluster the guard {time: true}, distinct from no guard. Buffering then created one buffer per guard for equivalent accesses --- devito/ir/clusters/algorithms.py | 3 +++ tests/test_buffering.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/devito/ir/clusters/algorithms.py b/devito/ir/clusters/algorithms.py index f594d35203..ed640e29ed 100644 --- a/devito/ir/clusters/algorithms.py +++ b/devito/ir/clusters/algorithms.py @@ -366,6 +366,9 @@ def guard(clusters): else: guards = {d: mode(*v, evaluate=False) for d, v in guards.items()} + # A condition folded to `true` (e.g., `time >= 0`) imposes no guard + guards = {d: v for d, v in guards.items() if v is not sympy.true} + # Construct a guarded Cluster processed.append(c.rebuild(exprs=exprs, guards=guards)) diff --git a/tests/test_buffering.py b/tests/test_buffering.py index 29c864e848..fa9b7d3562 100644 --- a/tests/test_buffering.py +++ b/tests/test_buffering.py @@ -1179,3 +1179,34 @@ def test_buffering_multi_cond(factor): # The last sample is on the subsampled grid, hence no extra slot expected[-2:] = [ntmod - 1, 0] assert np.allclose(f_all.data[:, 11, 11], expected) + + +def test_true_condition_single_buffer(): + """ + A condition folded to `true` (`time` is nonnegative, so `time >= 0` is + `true`) imposes no guard. Reads of `f` under it and unguarded reads of `f` + must thus share one buffer. + """ + grid = Grid(shape=(4, 4)) + time = grid.time_dim + nt = 4 + + f = TimeFunction(name='f', grid=grid, time_order=0, space_order=0, save=nt) + g = TimeFunction(name='g', grid=grid, time_order=0, space_order=0) + h = TimeFunction(name='h', grid=grid, time_order=0, space_order=0) + for i in range(nt): + f.data[i] = i + + ctrue = ConditionalDimension(name='ctrue', parent=time, condition=time >= 0, + relation='strict') + + eqs = [Eq(g, g + f, implicit_dims=ctrue), Eq(h, h + 2*f)] + + op = Operator(eqs, opt='buffering', name='TrueCondition') + + buffers = [i for i in FindSymbols().visit(op.body) if i.is_Array and i._mem_heap] + assert len(buffers) == 1 + + op.apply(time_m=0, time_M=nt-1) + assert np.allclose(g.data[0], 6.) + assert np.allclose(h.data[0], 12.)