Skip to content

Commit 0fb9039

Browse files
committed
compiler: Fixup Mod C vs SymPy diverging semantics
1 parent 3fcd485 commit 0fb9039

4 files changed

Lines changed: 98 additions & 5 deletions

File tree

‎devito/ir/cgen/printer.py‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from devito import configuration
1818
from devito.arch.compiler import AOMPCompiler
19+
from devito.symbolics.extended_sympy import BitwiseAnd
1920
from devito.symbolics.inspection import has_integer_args, sympy_dtype
2021
from devito.symbolics.queries import q_leaf
2122
from devito.tools import (
@@ -255,7 +256,26 @@ def _print_RoundUp(self, expr):
255256
return f'ROUND_UP({value}, {step})'
256257

257258
def _print_Mod(self, expr):
258-
"""Print a Mod as a C-like %-based operation."""
259+
"""
260+
Print a Mod as an integer remainder or a power-of-two mask.
261+
262+
Python's `%` and SymPy's `Mod` give a nonnegative result for a positive
263+
divisor, whereas C's `%` can be negative when the dividend is negative.
264+
For example, `Mod(-1, 4) == 3`, but C's `-1 % 4 == -1`. For integer
265+
operands and a positive power-of-two divisor `b`, emit `a & (b - 1)`
266+
unless `a` is known nonnegative, preserving the Python/SymPy semantics.
267+
"""
268+
a, b = expr.args
269+
270+
# Unlike C's signed remainder, a mask preserves `Mod`'s nonnegative
271+
# result for positive power-of-two divisors
272+
if b.is_Integer and \
273+
b > 0 and \
274+
not (int(b) & (int(b) - 1)) and \
275+
has_integer_args(a, b) and \
276+
a.is_nonnegative is not True:
277+
return f'({self._print(BitwiseAnd(a, b - 1))})'
278+
259279
args = [f'({self._print(a)})' for a in expr.args]
260280
return '%'.join(args)
261281

‎devito/symbolics/queries.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ def q_positive(expr):
232232
False simply means that it was not possible to determine an answer
233233
to the query `X > 0`.
234234
"""
235+
if expr.is_Number:
236+
return expr.is_positive is True
237+
235238
if not expr.is_Add:
236239
return False
237240

‎devito/types/dimension.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ class TimeDimension(BasicDimension):
483483
"""
484484

485485
is_Time = True
486+
is_nonnegative = True
486487

487488

488489
class DerivedDimension(BasicDimension):

‎tests/test_symbolics.py‎

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
from devito import ( # noqa
99
Abs, Conj, Constant, Dimension, Eq, Function, Ge, Grid, Gt, Imag, Le, Lt, Max, Min,
10-
Operator, Real, SubDimension, SubDomain, TimeFunction, configuration, cos, norm, sin,
11-
solve, switchconfig
10+
Operator, Real, SubDimension, SubDomain, TimeDimension, TimeFunction, configuration,
11+
cos, norm, sin, solve, switchconfig
1212
)
1313
from devito.finite_differences.differentiable import Mul, SafeInv, Weights
1414
from devito.ir import Expression, FindNodes, ccode
@@ -17,8 +17,9 @@
1717
from devito.symbolics import ( # noqa
1818
INT, BaseCast, CallFromPointer, Cast, DefFunction, FieldFromComposite,
1919
FieldFromPointer, IntDiv, ListInitializer, Namespace, ReservedWord, RoundUp, Rvalue,
20-
SizeOf, VectorAccess, evalrel, pow_to_mul, retrieve_derivatives, retrieve_functions,
21-
retrieve_indexed, subs_if_composite, uxreplace, xreplace_indices
20+
SizeOf, VectorAccess, evalrel, pow_to_mul, q_negative, q_positive,
21+
retrieve_derivatives, retrieve_functions, retrieve_indexed, subs_if_composite,
22+
uxreplace, xreplace_indices
2223
)
2324
from devito.tools import CustomDtype, as_tuple, dtypes_vector_mapper
2425
from devito.types import (
@@ -385,6 +386,74 @@ def test_intdiv():
385386
assert ccode(v) == 'b*((a + b) / 2) + 3'
386387

387388

389+
def test_mod_printing():
390+
a = dSymbol('a', dtype=np.int32)
391+
b = dSymbol('b', dtype=np.int32)
392+
p = Symbol('p', integer=True, nonnegative=True)
393+
394+
assert ccode(sympy.Mod(a - b + 3, 4)) == '((a - b + 3) & 3)'
395+
assert ccode(sympy.Mod(a, 8)) == '(a & 7)'
396+
397+
# Keep the existing remainder for known nonnegative dividends and other divisors
398+
assert ccode(sympy.Mod(p, 4)) == '(p)%(4)'
399+
assert ccode(sympy.Mod(a, 3)) == '(a)%(3)'
400+
assert ccode(sympy.Mod(a, b)) == '(a)%(b)'
401+
402+
403+
def test_mod_time_printing():
404+
time = TimeDimension(name='time')
405+
shift = dSymbol('shift', dtype=np.int32)
406+
407+
assert time.is_nonnegative is True
408+
assert time.is_positive is None
409+
assert ccode(sympy.Mod(time, 2)) == '(time)%(2)'
410+
assert ccode(sympy.Mod(time + 1, 2)) == '(time + 1)%(2)'
411+
412+
# A shifted time index may still be negative
413+
assert ccode(sympy.Mod(time - shift, 4)) == '((time - shift) & 3)'
414+
415+
416+
@pytest.mark.parametrize('divisor', [4, 8])
417+
def test_mod_negative(divisor):
418+
grid = Grid(shape=(65,))
419+
x, = grid.dimensions
420+
421+
f = Function(name='f', grid=grid, space_order=0, dtype=np.int32)
422+
shift = Constant(name='shift', dtype=np.int32, value=32)
423+
424+
# Also exercise precedence when Mod is nested in arithmetic
425+
eq = Eq(f, 3*sympy.Mod(x - shift, divisor) + 1)
426+
427+
op = Operator(eq, name='mod_negative', opt='noop')
428+
op.apply()
429+
430+
expected = 3*((np.arange(65) - 32) % divisor) + 1
431+
np.testing.assert_array_equal(np.asarray(f.data), expected)
432+
433+
434+
def test_sign_queries():
435+
for v in (sympy.S.One, sympy.Rational(1, 2), sympy.Float(0.5)):
436+
assert q_positive(v)
437+
assert q_negative(-v)
438+
assert not q_positive(-v)
439+
assert not q_negative(v)
440+
441+
assert not q_positive(sympy.S.Zero)
442+
assert not q_negative(sympy.S.Zero)
443+
444+
a = dSymbol('a', dtype=np.int32)
445+
r = sympy.Mod(a, 4)
446+
447+
assert q_positive(8 - r)
448+
assert q_positive(4 - r)
449+
assert q_negative(r - 8)
450+
# Zero and unknown signs must not be classified as strictly positive
451+
assert not q_positive(3 - r)
452+
assert not q_positive(r)
453+
assert not q_positive(a)
454+
assert not q_negative(a)
455+
456+
388457
def test_safeinv():
389458
grid = Grid(shape=(11, 11))
390459
x, y = grid.dimensions

0 commit comments

Comments
 (0)