|
8 | 8 | ) |
9 | 9 | from devito.data import LEFT, RIGHT, Decomposition, convert_index, loc_data_idx |
10 | 10 | 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 |
11 | 15 | from devito.ir import ccode |
12 | 16 | from devito.tools import as_tuple |
13 | 17 | from devito.types import Scalar |
@@ -624,6 +628,190 @@ def test_reshape_iterable(self): |
624 | 628 | assert d.reshape((1, 3, 10, 11, 14)) == Decomposition([[0], [1], [], [2, 3]], 2) |
625 | 629 |
|
626 | 630 |
|
| 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 | + |
627 | 815 | class TestDataDistributed: |
628 | 816 |
|
629 | 817 | """ |
|
0 commit comments