@@ -504,6 +504,80 @@ namespace xt
504504 ASSERT_EQ (expected8, xt::roll (e2 , -2 , /* axis*/ 2 ));
505505 }
506506
507+ TEST (xmanipulation, roll_multi_axis)
508+ {
509+ // Test 1: Basic 2D multi-axis roll
510+ xarray<double > e1 = {{1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 }};
511+
512+ // Roll 1 on axis 0, 2 on axis 1
513+ xarray<double > expected1 = {{8 , 9 , 7 }, {2 , 3 , 1 }, {5 , 6 , 4 }};
514+ ASSERT_EQ (expected1, xt::roll (e1 , {1 , 2 }, {0 , 1 }));
515+
516+ // Verify equivalence with sequential single-axis rolls
517+ auto sequential_result = xt::roll (xt::roll (e1 , 1 , 0 ), 2 , 1 );
518+ ASSERT_EQ (expected1, sequential_result);
519+
520+ // Test 2: std::array as input
521+ std::array<int , 2 > shifts = {1 , 2 };
522+ std::array<int , 2 > axes = {0 , 1 };
523+ ASSERT_EQ (expected1, xt::roll (e1 , shifts, axes));
524+
525+ // Test 3: std::vector as input
526+ std::vector<int > shifts_v = {1 , 2 };
527+ std::vector<int > axes_v = {0 , 1 };
528+ ASSERT_EQ (expected1, xt::roll (e1 , shifts_v, axes_v));
529+
530+ // Test 4: Negative shifts
531+ xarray<double > expected4 = {{6 , 4 , 5 }, {9 , 7 , 8 }, {3 , 1 , 2 }};
532+ ASSERT_EQ (expected4, xt::roll (e1 , {-1 , -2 }, {0 , 1 }));
533+
534+ // Test 5: Negative axis indices
535+ ASSERT_EQ (expected1, xt::roll (e1 , {1 , 2 }, {-2 , -1 }));
536+
537+ // Test 6: 3D array
538+ xarray<double > e3 = {{{1 , 2 }, {3 , 4 }}, {{5 , 6 }, {7 , 8 }}};
539+ xarray<double > expected6 = {{{8 , 7 }, {6 , 5 }}, {{4 , 3 }, {2 , 1 }}};
540+ ASSERT_EQ (expected6, xt::roll (e3 , {1 , 1 , 1 }, {0 , 1 , 2 }));
541+
542+ // Test 7: Single axis via multi-axis interface (should match single-axis version)
543+ xarray<double > expected7 = {{7 , 8 , 9 }, {1 , 2 , 3 }, {4 , 5 , 6 }};
544+ ASSERT_EQ (expected7, xt::roll (e1 , {1 }, {0 }));
545+ ASSERT_EQ (expected7, xt::roll (e1 , 1 , 0 ));
546+
547+ // Test 8: Empty shifts (should return copy of original)
548+ std::vector<int > empty_shifts;
549+ std::vector<int > empty_axes;
550+ ASSERT_EQ (e1 , xt::roll (e1 , empty_shifts, empty_axes));
551+
552+ // Test 9: Large shift values (should wrap around)
553+ // shift of 10 on axis 0 (size 3) is equivalent to shift of 1
554+ ASSERT_EQ (xt::roll (e1 , {1 }, {0 }), xt::roll (e1 , {10 }, {0 }));
555+
556+ // Test 10: Same axis appears multiple times (shifts accumulate)
557+ // NumPy: np.roll(a, (1, 2), axis=(0, 0)) equals np.roll(a, 3, axis=0)
558+ xarray<double > expected10 = xt::roll (e1 , 3 , 0 );
559+ ASSERT_EQ (expected10, xt::roll (e1 , {1 , 2 }, {0 , 0 }));
560+
561+ // Test 11: xtensor (fixed dimension) instead of xarray
562+ xt::xtensor<double , 2 > t1 = {{1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 }};
563+ xt::xtensor<double , 2 > t_expected = {{8 , 9 , 7 }, {2 , 3 , 1 }, {5 , 6 , 4 }};
564+ ASSERT_EQ (t_expected, xt::roll (t1, {1 , 2 }, {0 , 1 }));
565+
566+ // Test 12: 1D array multi-axis operation (only one axis)
567+ xarray<double > e1d = {1 , 2 , 3 , 4 , 5 };
568+ xarray<double > expected1d = {4 , 5 , 1 , 2 , 3 };
569+ ASSERT_EQ (expected1d, xt::roll (e1d, {2 }, {0 }));
570+
571+ // Test 13: Column-major layout (result should be layout-independent)
572+ xarray<double , layout_type::column_major> cm = {{1 , 2 , 3 }, {4 , 5 , 6 }};
573+ xarray<double > expected_cm = {{3 , 1 , 2 }, {6 , 4 , 5 }};
574+ ASSERT_EQ (expected_cm, xt::roll (cm, {1 }, {1 }));
575+
576+ // Test 14: Column-major with multi-axis roll
577+ xarray<double > expected_cm2 = {{6 , 4 , 5 }, {3 , 1 , 2 }};
578+ ASSERT_EQ (expected_cm2, xt::roll (cm, {1 , 1 }, {0 , 1 }));
579+ }
580+
507581 TEST (xmanipulation, repeat_all_elements_of_axis_0_of_int_array_2_times)
508582 {
509583 xarray<int > array = {
0 commit comments