Skip to content

data: Fix negative slice bounds along modulo-indexed Dimensions - #3086

Open
MaxFreedomPollard wants to merge 1 commit into
devitocodes:mainfrom
MaxFreedomPollard:fix/modulo-negative-slicing
Open

MaxFreedomPollard wants to merge 1 commit into
devitocodes:mainfrom
MaxFreedomPollard:fix/modulo-negative-slicing

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

Slicing the time axis of a buffered TimeFunction with a negative bound selects the wrong time slots, both when reading and when writing. With time_order=2, the usual setup for a second order wave equation, on main:

import numpy as np
from devito import Grid, TimeFunction

grid = Grid(shape=(4,))
u = TimeFunction(name='u', grid=grid, time_order=2)
u.data[:] = np.arange(3)[:, None]

u.data[-1:, 0]  # [1., 2.], NumPy gives [2.]
u.data[-2:, 0]  # [2.], NumPy gives [1., 2.]
u.data[:-1, 0]  # [], NumPy gives [0., 1.]

u.data[:-1] = 7                 # writes nothing
u.data[-1:] = 9                 # also overwrites slot 1
u.data[-2:] = np.zeros((2, 4))  # ValueError: could not broadcast input array from shape (2,4) into shape (1,4)

The default time_order=1 is affected too: u.data[:-1] comes back empty there as well.

The time Dimension of a buffered TimeFunction is modulo-indexed, so Data._index_glb_to_loc maps each index through index_apply_modulo in devito/data/utils.py. For a negative slice bound that function returns -(idx.start % modulo), or -(idx.stop % (modulo + 1)) for the stop. Python's % already gives a nonnegative remainder for a negative operand, so with three slots a start of -1 becomes -2, a start of -2 becomes -1, and a stop of -1 becomes -3.

The fix wraps the magnitude of a negative bound and keeps its sign, i.e. -(-idx.start % modulo) and -(-idx.stop % (modulo + 1)). A negative bound within the buffer now selects the same entries as in NumPy. I checked every slice with a start in [-n, n) or None, a stop in [-n, n] or None, and a step of None, ±1 or ±2, for buffers of 1 to 7 slots. All of them now match NumPy. Before the fix, 84 of the 280 cases for three slots did not. A negative bound beyond the buffer wraps the same way an integer index does, so u.data[-4:] now starts at the slot u.data[-4] refers to. Nonnegative bounds are unchanged. So is any Data without modulo indexing, which covers every Function, saved TimeFunction and SparseTimeFunction.

Verified on macOS (Apple silicon, Python 3.12, DEVITO_ARCH=clang, DEVITO_LANGUAGE=C):

  • The new tests/test_data.py::TestDataBasic::test_negative_slicing_modulo reads and writes through eight slices with negative bounds and compares the results against NumPy. Six of the eight fail on main and all pass with the fix.
  • pytest -m "not parallel" tests/test_data.py: 97 passed, 2 skipped.
  • tests/test_builtins.py, test_buffering.py, test_checkpointing.py, test_dimension.py, test_save.py, test_timestepping.py, test_operator.py and test_docstrings.py: 484 passed. The 11 failures are environmental and identical on main: 8 cannot download scipy's ascent image, and 3 need OpenMP, which Apple clang lacks.
  • tests/test_sparse.py, test_interpolation.py and test_tensors.py: 268 passed.
  • isort, ruff check --preview, flake8 and typos are clean.

🤖 Generated with Claude Code

`index_apply_modulo` negated `idx.start % modulo`, which Python already
makes nonnegative, so a negative bound on the time axis of a buffered
TimeFunction picked the wrong slots. With time_order=2, `u.data[-1:]`
returned slots 1 and 2, `u.data[:-1]` returned nothing, and assignments
through such slices wrote to the wrong slots or to none at all.

Wrap the magnitude of a negative bound instead, so that a negative bound
within the buffer selects the same entries as in NumPy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.78%. Comparing base (a9e3f31) to head (cc46aa1).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3086      +/-   ##
==========================================
- Coverage   84.27%   79.78%   -4.50%     
==========================================
  Files         258      258              
  Lines       57421    57432      +11     
  Branches     4911     4911              
==========================================
- Hits        48392    45822    -2570     
- Misses       8205    10729    +2524     
- Partials      824      881      +57     
Flag Coverage Δ
pytest-gpu-aomp-amdgpuX ?
pytest-gpu-gcc- ?
pytest-gpu-icx- ?
pytest-gpu-nvc-nvidiaX ?

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.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants