Skip to main content

p3_matrix/
strided.rs

1use crate::Matrix;
2use crate::row_index_mapped::{RowIndexMap, RowIndexMappedView};
3
4/// A vertical row-mapping strategy that selects every `stride`-th row from an inner matrix,
5/// starting at a fixed `offset`.
6///
7/// This enables vertical striding like selecting rows: `offset`, `offset + stride`, etc.
8#[derive(Debug)]
9pub struct VerticallyStridedRowIndexMap {
10    /// The number of rows in the resulting view.
11    height: usize,
12    /// The step size between selected rows in the inner matrix.
13    stride: usize,
14    /// The offset to start the stride from.
15    offset: usize,
16}
17
18pub type VerticallyStridedMatrixView<Inner> =
19    RowIndexMappedView<VerticallyStridedRowIndexMap, Inner>;
20
21impl VerticallyStridedRowIndexMap {
22    /// Create a new vertically strided view over a matrix.
23    ///
24    /// This selects rows in the inner matrix starting from `offset`, and then every `stride` rows after.
25    /// Any choice of `offset` is valid.
26    /// An `offset` at or past the inner height yields an empty view.
27    ///
28    /// # Arguments
29    /// - `inner`: The inner matrix to view.
30    /// - `stride`: The number of rows between each selected row.
31    /// - `offset`: The inner row index of the first selected row.
32    ///
33    /// # Panics
34    /// Panics if `stride` is zero.
35    pub fn new_view<T: Send + Sync + Clone, Inner: Matrix<T>>(
36        inner: Inner,
37        stride: usize,
38        offset: usize,
39    ) -> VerticallyStridedMatrixView<Inner> {
40        // View row i maps to inner row offset + i * stride, valid while < h.
41        // Count of valid rows: ceil((h - offset) / stride), saturating to 0 when offset >= h.
42        let height = inner.height().saturating_sub(offset).div_ceil(stride);
43        RowIndexMappedView {
44            index_map: Self {
45                height,
46                stride,
47                offset,
48            },
49            inner,
50        }
51    }
52}
53
54impl RowIndexMap for VerticallyStridedRowIndexMap {
55    fn height(&self) -> usize {
56        self.height
57    }
58
59    fn map_row_index(&self, r: usize) -> usize {
60        r * self.stride + self.offset
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use alloc::vec;
67
68    use super::*;
69    use crate::{Matrix, RowMajorMatrix};
70
71    fn sample_matrix() -> RowMajorMatrix<i32> {
72        // A 5x3 matrix:
73        // [10, 11, 12]
74        // [20, 21, 22]
75        // [30, 31, 32]
76        // [40, 41, 42]
77        // [50, 51, 52]
78        RowMajorMatrix::new(
79            vec![10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52],
80            3,
81        )
82    }
83
84    #[test]
85    fn test_vertically_strided_view_stride_1_offset_0() {
86        let matrix = sample_matrix();
87        let view = VerticallyStridedRowIndexMap::new_view(matrix, 1, 0);
88
89        assert_eq!(view.height(), 5);
90        assert_eq!(view.width(), 3);
91
92        assert_eq!(view.get(0, 0), Some(10));
93        assert_eq!(view.get(1, 1), Some(21));
94        unsafe {
95            assert_eq!(view.get_unchecked(4, 2), 52);
96        }
97        assert_eq!(view.get(5, 0), None); // out of bounds
98        assert_eq!(view.get(0, 3), None); // out of bounds
99    }
100
101    #[test]
102    fn test_vertically_strided_view_stride_2_offset_0() {
103        let matrix = sample_matrix();
104        let view = VerticallyStridedRowIndexMap::new_view(matrix, 2, 0);
105
106        assert_eq!(view.height(), 3);
107        assert_eq!(view.get(0, 0), Some(10)); // row 0
108        unsafe {
109            assert_eq!(view.get_unchecked(1, 1), 31); // row 2
110            assert_eq!(view.get_unchecked(2, 2), 52); // row 4
111        }
112        assert_eq!(view.get(0, 3), None); // out of bounds
113    }
114
115    #[test]
116    fn test_vertically_strided_view_stride_2_offset_1() {
117        let matrix = sample_matrix();
118        let view = VerticallyStridedRowIndexMap::new_view(matrix, 2, 1);
119
120        assert_eq!(view.height(), 2);
121        assert_eq!(view.get(0, 0), Some(20)); // row 1
122        unsafe {
123            assert_eq!(view.get_unchecked(1, 1), 41);
124        } // row 3
125    }
126
127    #[test]
128    fn test_vertically_strided_view_stride_3_offset_0() {
129        let matrix = sample_matrix();
130        let view = VerticallyStridedRowIndexMap::new_view(matrix, 3, 0);
131
132        assert_eq!(view.height(), 2);
133        assert_eq!(view.get(0, 0), Some(10)); // row 0
134        assert_eq!(view.get(1, 1), Some(41)); // row 3
135    }
136
137    #[test]
138    fn test_vertically_strided_view_stride_3_offset_1() {
139        let matrix = sample_matrix();
140        let view = VerticallyStridedRowIndexMap::new_view(matrix, 3, 1);
141
142        assert_eq!(view.height(), 2);
143        unsafe {
144            assert_eq!(view.get_unchecked(0, 0), 20); // row 1
145            assert_eq!(view.get_unchecked(1, 1), 51); // row 4
146        }
147    }
148
149    #[test]
150    fn test_vertically_strided_view_stride_3_offset_2() {
151        let matrix = sample_matrix();
152        let view = VerticallyStridedRowIndexMap::new_view(matrix, 3, 2);
153
154        assert_eq!(view.height(), 1);
155        assert_eq!(view.get(0, 2), Some(32)); // row 2
156    }
157
158    #[test]
159    fn test_vertically_strided_view_stride_greater_than_height() {
160        let matrix = sample_matrix();
161        let view = VerticallyStridedRowIndexMap::new_view(matrix, 10, 0);
162
163        assert_eq!(view.height(), 1);
164        assert_eq!(view.get(0, 0), Some(10)); // row 0
165    }
166
167    #[test]
168    fn test_vertically_strided_view_stride_greater_than_height_with_valid_offset() {
169        let matrix = sample_matrix(); // height = 5
170        let view = VerticallyStridedRowIndexMap::new_view(matrix, 10, 4);
171
172        // offset == 4 < height == 5 → view selects row 4
173        assert_eq!(view.height(), 1);
174        assert_eq!(view.get(0, 2), Some(52)); // row 4
175    }
176
177    #[test]
178    fn test_vertically_strided_view_stride_greater_than_height_with_offset_beyond_height() {
179        let matrix = sample_matrix(); // height = 5
180        let view = VerticallyStridedRowIndexMap::new_view(matrix, 10, 6);
181
182        // offset == 6 > height == 5 → no valid row
183        assert_eq!(view.height(), 0);
184        assert_eq!(view.get(0, 0), None); // out of bounds
185    }
186
187    #[test]
188    fn test_vertically_strided_view_offset_greater_than_stride() {
189        // Regression: with offset >= stride the old height formula over-reported,
190        // letting view rows map past the inner height.
191        //
192        //     h = 5, stride = 2, offset = 3 → inner rows 3, 5, 7, ... → only row 3 in bounds
193        let matrix = sample_matrix();
194        let view = VerticallyStridedRowIndexMap::new_view(matrix, 2, 3);
195
196        assert_eq!(view.height(), 1);
197        // The single view row is inner row 3.
198        assert_eq!(view.get(0, 0), Some(40));
199        // View row 1 would be inner row 5, past the inner height → rejected.
200        assert_eq!(view.get(1, 0), None);
201    }
202
203    #[test]
204    fn test_vertically_strided_view_offset_equal_to_stride() {
205        // h = 6, stride = 2, offset = 2 → inner rows 2, 4, 6, ... → rows 2 and 4 in bounds.
206        let matrix = RowMajorMatrix::new(vec![10, 20, 30, 40, 50, 60], 1);
207        let view = VerticallyStridedRowIndexMap::new_view(matrix, 2, 2);
208
209        assert_eq!(view.height(), 2);
210        assert_eq!(view.get(0, 0), Some(30)); // inner row 2
211        assert_eq!(view.get(1, 0), Some(50)); // inner row 4
212        // View row 2 would be inner row 6, past the inner height → rejected.
213        assert_eq!(view.get(2, 0), None);
214    }
215
216    #[test]
217    fn test_vertically_strided_view_exhaustive_height_and_bounds() {
218        // Invariant: for any (stride, offset), the view height matches a brute-force count
219        // and every view row reads the expected inner row.
220        for stride in 1..=7usize {
221            // Sweep offsets covering offset < stride, stride <= offset < h, and offset >= h.
222            for offset in 0..=12usize {
223                let matrix = sample_matrix();
224                let h = matrix.height();
225                let view = VerticallyStridedRowIndexMap::new_view(matrix, stride, offset);
226
227                // Brute-force reference: count inner rows offset + i * stride < h.
228                let expected_height = (offset..h).step_by(stride).count();
229                assert_eq!(
230                    view.height(),
231                    expected_height,
232                    "height mismatch for stride={stride}, offset={offset}"
233                );
234
235                // Every view row must read back the matching inner row,
236                // proving the mapped index stays in bounds.
237                for (i, inner_row) in (offset..h).step_by(stride).enumerate() {
238                    assert_eq!(
239                        view.get(i, 0),
240                        Some(10 * (inner_row as i32 + 1)),
241                        "wrong row for stride={stride}, offset={offset}, view row {i}"
242                    );
243                }
244
245                // The first row past the reported height must be rejected.
246                assert_eq!(view.get(expected_height, 0), None);
247            }
248        }
249    }
250}