Skip to content

Commit bf5e2a7

Browse files
committed
tests: Fold distributed-engine unit tests into test_data.py
These are serial unit tests of devito/data internals (Selection, Layout, result_dims, index_has_array), same as TestDecomposition, so they belong beside it rather than in a separate file. Move the four classes into test_data.py (aliasing the engine's Scalar to avoid clashing with devito.types.Scalar) and drop tests/test_distributed_engine.py.
1 parent a413b30 commit bf5e2a7

2 files changed

Lines changed: 188 additions & 195 deletions

File tree

tests/test_data.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
)
99
from devito.data import LEFT, RIGHT, Decomposition, convert_index, loc_data_idx
1010
from devito.data.allocators import DataReference
11+
from devito.data.distributed.layout import Layout
12+
from devito.data.distributed.selection import Affine, Explicit
13+
from devito.data.distributed.selection import Scalar as IndexScalar
14+
from devito.data.distributed.selection import Selection, index_has_array, result_dims
1115
from devito.ir import ccode
1216
from devito.tools import as_tuple
1317
from devito.types import Scalar
@@ -624,6 +628,190 @@ def test_reshape_iterable(self):
624628
assert d.reshape((1, 3, 10, 11, 14)) == Decomposition([[0], [1], [], [2, 3]], 2)
625629

626630

631+
# A reference global shape for the serial engine tests below; indices are
632+
# validated against the NumPy result on an array of this shape.
633+
ENGINE_SHAPE = (4, 5, 6)
634+
635+
636+
class TestSelection:
637+
638+
"""``Selection`` encodes NumPy indexing semantics, layout-independent."""
639+
640+
@pytest.mark.parametrize('idx', [
641+
# basic indexing
642+
(2,),
643+
(slice(None),),
644+
(slice(1, 4, 2),),
645+
(slice(None, None, -1),),
646+
(slice(3, 0, -2),),
647+
(slice(None), slice(None), 3),
648+
(1, 2, 3),
649+
(-1, -2),
650+
(slice(-3, -1),),
651+
# ellipsis / padding
652+
(Ellipsis, 2),
653+
(1, Ellipsis),
654+
(Ellipsis,),
655+
# advanced (array) indexing
656+
(np.array([0, 2, 3]),),
657+
(np.array([0, 2]), slice(1, 4)),
658+
(slice(None), np.array([0, 1, 4])),
659+
(np.array([[0, 1], [2, 3]]),),
660+
# contiguous advanced axes (stay in place)
661+
(np.array([0, 1]), np.array([2, 3])),
662+
# advanced axes separated by a basic index (block moves to front)
663+
(np.array([0, 1]), 2, np.array([0, 3])),
664+
(np.array([0, 1]), slice(None), np.array([0, 3])),
665+
# broadcast advanced indices
666+
(np.array([[0], [1]]), np.array([0, 2, 4])),
667+
# boolean masks
668+
(np.array([True, False, True, True]),),
669+
(slice(None), np.array([True, False, True, True, False])),
670+
])
671+
def test_result_shape_matches_numpy(self, idx):
672+
"""The induced result shape matches NumPy's for the same index."""
673+
ref = np.empty(ENGINE_SHAPE)
674+
selection = Selection.from_index(idx, ENGINE_SHAPE)
675+
assert selection.result_shape == ref[idx].shape
676+
677+
def test_selector_types(self):
678+
"""Each axis is classified as Scalar / Affine / Explicit as expected."""
679+
selection = Selection.from_index((2, slice(1, 4), np.array([0, 1])),
680+
ENGINE_SHAPE)
681+
s0, s1, s2 = selection.selectors
682+
assert isinstance(s0, IndexScalar) and s0.index == 2
683+
assert isinstance(s1, Affine) and (s1.start, s1.stop, s1.step) == (1, 4, 1)
684+
assert isinstance(s2, Explicit) and list(s2.coords) == [0, 1]
685+
686+
def test_negative_scalar_normalized(self):
687+
"""A negative scalar index is normalized to a non-negative one."""
688+
selection = Selection.from_index((-1,), ENGINE_SHAPE)
689+
assert selection.selectors[0] == IndexScalar(ENGINE_SHAPE[0] - 1)
690+
691+
def test_negative_array_normalized(self):
692+
"""Negative entries in an advanced index are wrapped into range."""
693+
selection = Selection.from_index((np.array([-1, -2]),), ENGINE_SHAPE)
694+
assert list(selection.selectors[0].coords) == [ENGINE_SHAPE[0] - 1,
695+
ENGINE_SHAPE[0] - 2]
696+
697+
def test_advanced_at_front_detection(self):
698+
"""Separated advanced axes set ``advanced_at_front``; contiguous don't."""
699+
sep = Selection.from_index((np.array([0, 1]), 2, np.array([0, 3])),
700+
ENGINE_SHAPE)
701+
cont = Selection.from_index((np.array([0, 1]), np.array([0, 3])),
702+
ENGINE_SHAPE)
703+
assert sep.advanced_at_front is True
704+
assert cont.advanced_at_front is False
705+
706+
def test_npoints_and_is_advanced(self):
707+
sel = Selection.from_index((np.array([[0], [1]]), np.array([0, 2, 4])),
708+
ENGINE_SHAPE)
709+
assert sel.is_advanced is True
710+
assert sel.advanced_shape == (2, 3)
711+
assert sel.npoints == 6
712+
basic = Selection.from_index((slice(None),), ENGINE_SHAPE)
713+
assert basic.is_advanced is False
714+
assert basic.npoints == 1
715+
716+
def test_scalar_out_of_bounds(self):
717+
with pytest.raises(IndexError):
718+
Selection.from_index((4,), ENGINE_SHAPE)
719+
720+
def test_too_many_indices(self):
721+
with pytest.raises(IndexError):
722+
Selection.from_index((1, 2, 3, 4), ENGINE_SHAPE)
723+
724+
def test_newaxis_unsupported(self):
725+
with pytest.raises(NotImplementedError):
726+
Selection.from_index((np.newaxis, slice(None)), ENGINE_SHAPE)
727+
728+
729+
class TestResultDims:
730+
731+
"""``result_dims`` is the single source of result-axis ordering."""
732+
733+
def test_basic_only(self):
734+
sel = Selection.from_index((2, slice(None), slice(None)), ENGINE_SHAPE)
735+
assert sel.result_dims == [('basic', 1), ('basic', 2)]
736+
737+
def test_contiguous_advanced_in_place(self):
738+
sel = Selection.from_index((np.array([0, 1]), np.array([0, 3]), slice(None)),
739+
ENGINE_SHAPE)
740+
# advanced block sits where the (contiguous) advanced axes are
741+
assert sel.result_dims == [('adv', 0), ('basic', 2)]
742+
743+
def test_separated_advanced_moves_to_front(self):
744+
sel = Selection.from_index((np.array([0, 1]), slice(None), np.array([0, 3])),
745+
ENGINE_SHAPE)
746+
assert sel.result_dims == [('adv', 0), ('basic', 1)]
747+
748+
def test_result_shape_derives_from_dims(self):
749+
"""``result_shape`` is exactly the sizes of ``result_dims``, in order."""
750+
sel = Selection.from_index((np.array([0, 1]), slice(1, 4), np.array([0, 3])),
751+
ENGINE_SHAPE)
752+
sizes = tuple(sel.selectors[v].size if kind == 'basic'
753+
else sel.advanced_shape[v] for kind, v in sel.result_dims)
754+
assert sizes == sel.result_shape
755+
756+
def test_module_and_property_agree(self):
757+
sel = Selection.from_index((np.array([0, 1]), 2, np.array([0, 3])),
758+
ENGINE_SHAPE)
759+
assert sel.result_dims == result_dims(
760+
sel.selectors, sel.advanced_axes, sel.advanced_shape,
761+
sel.advanced_at_front)
762+
763+
764+
class TestIndexHasArray:
765+
766+
"""The cheap gate that keeps basic indexing off the routing path."""
767+
768+
@pytest.mark.parametrize('idx, ndim, expected', [
769+
((slice(None), 2), 2, False),
770+
(2, 2, False),
771+
(slice(1, 4), 2, False),
772+
((np.array([0, 1]), slice(None)), 2, True),
773+
(np.array([0, 1]), 1, True),
774+
(np.array([True, False]), 1, True),
775+
# legacy ``data[[i, j, k]]`` shorthand on an n-D object stays basic
776+
([0, 1, 2], 3, False),
777+
# a genuine 1-D list index on a 1-D object is advanced
778+
([0, 1, 2], 1, True),
779+
])
780+
def test_gate(self, idx, ndim, expected):
781+
assert index_has_array(idx, ndim) is expected
782+
783+
784+
class TestLayout:
785+
786+
"""Physical placement maps, computed locally from the decomposition."""
787+
788+
@pytest.fixture
789+
def decomposition(self):
790+
# 12 indices over 4 sub-ranks, uneven: [0,1,2] [3,4] [5,6,7] [8..11]
791+
return Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2)
792+
793+
def test_axis_maps(self, decomposition):
794+
layout = Layout(None, (decomposition, None), (12, 3))
795+
owner, local, sizes = layout.axis_maps(0)
796+
assert list(owner) == [0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3]
797+
assert list(local) == [0, 1, 2, 0, 1, 0, 1, 2, 0, 1, 2, 3]
798+
assert list(sizes) == [3, 2, 3, 4]
799+
800+
def test_distributed_and_replicated_axes(self, decomposition):
801+
layout = Layout(None, (decomposition, None, decomposition), (12, 3, 12))
802+
assert layout.distributed_axes == (0, 2)
803+
assert layout.replicated_axes == (1,)
804+
assert layout.replicated_size == 3
805+
assert layout.topology_shape == (4, 4)
806+
807+
def test_single_axis_coord_to_rank(self, decomposition):
808+
# Without ``all_coords`` (e.g. a sparse distributor) ranks lay out linearly
809+
class _Dist:
810+
nprocs = 4
811+
layout = Layout(_Dist(), (decomposition,), (12,))
812+
assert layout.coord_to_rank == {(0,): 0, (1,): 1, (2,): 2, (3,): 3}
813+
814+
627815
class TestDataDistributed:
628816

629817
"""

tests/test_distributed_engine.py

Lines changed: 0 additions & 195 deletions
This file was deleted.

0 commit comments

Comments
 (0)