Skip to main content

numrs2/
stride_tricks.rs

1use crate::array::Array;
2use crate::error::{NumRs2Error, Result};
3use scirs2_core::ndarray::{IxDyn, SliceInfo, SliceInfoElem};
4use std::fmt::Debug;
5
6/// Advanced stride manipulation utilities for NumRS2 arrays.
7///
8/// This module provides advanced functions for manipulating array strides,
9/// enabling sophisticated and memory-efficient array operations similar to
10/// NumPy's `numpy.lib.stride_tricks` module.
11/// Create a view of the given array with the specified strides without copying.
12///
13/// This is a lower-level function than `as_strided` as it directly manipulates
14/// the strides of the array. The returned array is a view of the original
15/// array with modified strides.
16///
17/// # Arguments
18///
19/// * `array` - The input array
20/// * `strides` - The new strides to use
21///
22/// # Returns
23///
24/// * `Ok(Array<T>)` - A view of the input array with the specified strides
25/// * `Err(NumRs2Error)` - Error if strides are invalid or dimension mismatch
26///
27/// # Examples
28///
29/// ```
30/// use numrs2::prelude::*;
31///
32/// let array = Array::from_vec(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).reshape(&[3, 3]);
33///
34/// // Create a view with stride 2 in both dimensions (every other element)
35/// let strided = set_strides(&array, &[2, 2]).expect("set_strides should succeed");
36/// assert_eq!(strided.shape(), vec![2, 2]);
37/// ```
38///
39/// # Safety
40///
41/// This function can be unsafe as it allows creating views that might go beyond
42/// the bounds of the original array if used incorrectly. The function attempts
43/// to validate the strides, but it's the caller's responsibility to ensure they
44/// are valid for the given array.
45pub fn set_strides<T>(array: &Array<T>, strides: &[isize]) -> Result<Array<T>>
46where
47    T: Clone + Debug,
48{
49    if strides.len() != array.ndim() {
50        return Err(NumRs2Error::DimensionMismatch(format!(
51            "Expected {} strides, got {}",
52            array.ndim(),
53            strides.len()
54        )));
55    }
56
57    let view = array.array().view();
58    let shape = array.shape();
59
60    // Create stride information for each dimension
61    let mut slice_info = Vec::with_capacity(array.ndim());
62
63    for (i, &stride) in strides.iter().enumerate() {
64        let dim_size = shape[i];
65
66        if stride == 0 {
67            return Err(NumRs2Error::InvalidOperation(format!(
68                "Stride for dimension {} cannot be zero",
69                i
70            )));
71        }
72
73        // If stride is positive, create a slice from 0 to dim_size with step stride
74        let start = if stride > 0 { 0 } else { dim_size as isize - 1 };
75        let end = if stride > 0 { dim_size as isize } else { -1 };
76
77        slice_info.push(SliceInfoElem::Slice {
78            start,
79            end: Some(end),
80            step: stride,
81        });
82    }
83
84    // Create the slice information
85    let slice_info = SliceInfo::<_, IxDyn, IxDyn>::try_from(slice_info)
86        .map_err(|_| NumRs2Error::InvalidOperation("Failed to create slice info".to_string()))?;
87
88    // Slice the array and return the view
89    let strided = view.slice(slice_info);
90    let result = Array::from_ndarray(strided.to_owned());
91    Ok(result)
92}
93
94/// Create a new view into the array with the given shape and strides.
95///
96/// This function is similar to NumPy's `numpy.lib.stride_tricks.as_strided`.
97/// It creates a view with a specific shape and strides without copying the data.
98///
99/// # Arguments
100///
101/// * `array` - The input array
102/// * `shape` - The shape of the new view
103/// * `strides` - The strides for the new view, **in elements** (not bytes).
104///   The value at output multi-index `idx` is
105///   `flat(array)[sum(idx[d] * strides[d] for d in 0..shape.len())]`, where
106///   `flat(array)` is `array`'s data read in row-major (C) order. A stride
107///   of `0` along an axis is allowed and repeats the same element for every
108///   index on that axis -- this is the mechanism `broadcast_to` is built on.
109///   Negative strides are allowed as long as every reachable offset stays
110///   within `[0, array.size())`; since index `0` always contributes offset
111///   `0` on every axis, a negative stride only stays in-bounds when its own
112///   axis has size `1` (compare `set_strides`, which rejects stride `0`
113///   outright because it slices rather than gathers).
114///
115/// # Returns
116///
117/// * `Ok(Array<T>)` - A view of the input array with the specified shape and strides
118/// * `Err(NumRs2Error)` - Error if `shape` and `strides` have different lengths,
119///   or if any offset reachable by `shape`/`strides` would fall outside the
120///   input array's data
121///
122/// # Examples
123///
124/// ```
125/// use numrs2::prelude::*;
126///
127/// let array = Array::from_vec(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).reshape(&[3, 3]);
128///
129/// // Sample every other row and column. [6, 2] is 2x the array's natural
130/// // (element) strides [3, 1], so this yields the four corners of the grid.
131/// let strided = as_strided(&array, &[2, 2], &[6, 2]).expect("as_strided should succeed");
132/// assert_eq!(strided.shape(), vec![2, 2]);
133/// assert_eq!(strided.to_vec(), vec![1, 3, 7, 9]);
134/// ```
135///
136/// # Safety
137///
138/// This function reads `array`'s data via `Array::to_vec()`, which flattens
139/// it in row-major (C) order; `strides` are interpreted against that
140/// flattened buffer, not against `array`'s own internal memory layout. Every
141/// offset reachable by `shape`/`strides` is bounds-checked before any data
142/// is read, so out-of-range parameters return `Err` rather than panicking or
143/// silently producing garbage -- but nothing stops `shape`/`strides` from
144/// describing a view whose elements overlap or repeat, which is inherent to
145/// `as_strided` and is the caller's responsibility to use correctly.
146pub fn as_strided<T>(array: &Array<T>, shape: &[usize], strides: &[isize]) -> Result<Array<T>>
147where
148    T: Clone + Debug,
149{
150    if shape.len() != strides.len() {
151        return Err(NumRs2Error::DimensionMismatch(format!(
152            "Shape and strides must have the same length, got {} and {}",
153            shape.len(),
154            strides.len()
155        )));
156    }
157
158    // Read the array's data in row-major (C) order; `strides` are
159    // interpreted against this flattened buffer (see the Safety section
160    // above), independently of `array`'s own ndim or internal layout.
161    let flat_data = array.to_vec();
162    let n = flat_data.len();
163
164    // A shape with any zero-sized dimension has zero total elements: there
165    // is nothing to gather, and no meaningful bounds to check.
166    let total_size: usize = shape.iter().product();
167    if total_size == 0 {
168        return Array::from_vec_shape(Vec::new(), shape);
169    }
170
171    // offset(idx) = sum(idx[d] * strides[d]) is a sum of independent
172    // per-axis terms: idx[d] ranges freely over 0..shape[d] regardless of
173    // the other axes. So the true minimum/maximum offset reachable across
174    // the *entire* output index space is exactly the sum of each axis's own
175    // minimum/maximum contribution -- and both extremes are always actually
176    // visited, since the index space is a full grid product.
177    let mut min_offset: isize = 0;
178    let mut max_offset: isize = 0;
179    for (&dim, &stride) in shape.iter().zip(strides.iter()) {
180        let extent = (dim as isize - 1) * stride;
181        if extent >= 0 {
182            max_offset += extent;
183        } else {
184            min_offset += extent;
185        }
186    }
187
188    if min_offset < 0 || max_offset >= n as isize {
189        return Err(NumRs2Error::InvalidOperation(format!(
190            "as_strided: shape {:?} with strides {:?} would access offsets in [{}, {}], \
191             out of bounds for an array of {} elements",
192            shape, strides, min_offset, max_offset, n
193        )));
194    }
195
196    // General N-D strided gather: for each output multi-index (recovered by
197    // unraveling the row-major linear index), read the element at the
198    // corresponding flat offset.
199    let mut result_data = Vec::with_capacity(total_size);
200    for linear in 0..total_size {
201        let idx = unravel_index(linear, shape);
202        let offset: isize = idx
203            .iter()
204            .zip(strides.iter())
205            .map(|(&i, &s)| i as isize * s)
206            .sum();
207        // In-bounds by construction: for every possible `idx`, `offset` is
208        // bracketed by [min_offset, max_offset] ⊆ [0, n), as validated above.
209        result_data.push(flat_data[offset as usize].clone());
210    }
211
212    Array::from_vec_shape(result_data, shape)
213}
214
215/// Convert a linear (row-major) index into a multi-dimensional index for
216/// `shape` -- the inverse of C-order flattening. The last axis varies
217/// fastest, matching `Array::to_vec()` / `Array::reshape()`'s row-major
218/// convention.
219fn unravel_index(mut linear: usize, shape: &[usize]) -> Vec<usize> {
220    let mut idx = vec![0usize; shape.len()];
221    for d in (0..shape.len()).rev() {
222        let dim = shape[d];
223        if dim > 0 {
224            idx[d] = linear % dim;
225            linear /= dim;
226        }
227    }
228    idx
229}
230
231/// Compute the row-major (C-order) element strides for `shape`: the strides
232/// a freshly-allocated, densely-packed array of this shape would have (the
233/// last axis has stride `1`, and each preceding axis's stride is the
234/// product of all the dimension sizes to its right).
235fn row_major_strides(shape: &[usize]) -> Vec<isize> {
236    let mut strides = vec![1isize; shape.len()];
237    for d in (0..shape.len().saturating_sub(1)).rev() {
238        strides[d] = strides[d + 1] * shape[d + 1] as isize;
239    }
240    strides
241}
242
243/// Create a sliding window view of an array.
244///
245/// This function creates a sliding window view of the input array with the given
246/// window shape. The sliding window moves along each dimension of the input array.
247///
248/// # Arguments
249///
250/// * `array` - The input array
251/// * `window_shape` - The shape of the sliding window
252/// * `step` - The step size for each dimension (default is 1)
253///
254/// # Returns
255///
256/// * `Ok(Array<T>)` - A view with shape (n1, n2, ..., k1, k2, ...) where (n1, n2, ...)
257///   is the number of valid positions of the sliding window, and (k1, k2, ...) is the
258///   window shape.
259/// * `Err(NumRs2Error)` - Error if parameters are invalid
260///
261/// # Examples
262///
263/// ```
264/// use numrs2::prelude::*;
265///
266/// let array = Array::from_vec(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).reshape(&[3, 3]);
267///
268/// // Create a 2x2 sliding window view of the array
269/// let windows = sliding_window_view(&array, &[2, 2], None).expect("sliding_window_view should succeed");
270/// assert_eq!(windows.shape(), vec![2, 2, 2, 2]);
271/// assert_eq!(
272///     windows.to_vec(),
273///     vec![1, 2, 4, 5, 2, 3, 5, 6, 4, 5, 7, 8, 5, 6, 8, 9]
274/// );
275/// ```
276pub fn sliding_window_view<T>(
277    array: &Array<T>,
278    window_shape: &[usize],
279    step: Option<&[usize]>,
280) -> Result<Array<T>>
281where
282    T: Clone + Debug,
283{
284    let step_values = match step {
285        Some(s) => {
286            if s.len() != array.ndim() {
287                return Err(NumRs2Error::DimensionMismatch(format!(
288                    "Step must have the same length as array dimensions, got {} and {}",
289                    s.len(),
290                    array.ndim()
291                )));
292            }
293            if s.contains(&0) {
294                return Err(NumRs2Error::InvalidOperation(
295                    "Step sizes must be greater than zero".to_string(),
296                ));
297            }
298            s.to_vec()
299        }
300        None => vec![1; array.ndim()],
301    };
302
303    if window_shape.len() != array.ndim() {
304        return Err(NumRs2Error::DimensionMismatch(format!(
305            "Window shape must have the same length as array dimensions, got {} and {}",
306            window_shape.len(),
307            array.ndim()
308        )));
309    }
310
311    // Calculate the number of valid window positions along each dimension.
312    let array_shape = array.shape();
313    let ndim = array.ndim();
314    let mut n_windows = Vec::with_capacity(ndim);
315
316    for i in 0..ndim {
317        let window_size = window_shape[i];
318        let step_size = step_values[i];
319        let dim_size = array_shape[i];
320
321        if window_size > dim_size {
322            return Err(NumRs2Error::InvalidOperation(format!(
323                "Window size {} exceeds array dimension {} of size {}",
324                window_size, i, dim_size
325            )));
326        }
327
328        n_windows.push((dim_size - window_size) / step_size + 1);
329    }
330
331    // Output shape is (n_windows..., window_shape...): one axis per input
332    // dimension counting window positions, followed by one axis per input
333    // dimension spanning the window itself.
334    let mut output_shape = n_windows;
335    output_shape.extend_from_slice(window_shape);
336
337    // Reuse as_strided (general for any ndim): the "window count" axes step
338    // through the array at `step * natural_stride`, and the appended
339    // "window shape" axes step one element at a time along the same
340    // original axis (natural_stride) -- exactly how NumPy implements
341    // sliding_window_view via as_strided.
342    let natural_stride = row_major_strides(&array_shape);
343    let mut combined_strides = Vec::with_capacity(ndim * 2);
344    for i in 0..ndim {
345        combined_strides.push(natural_stride[i] * step_values[i] as isize);
346    }
347    combined_strides.extend_from_slice(&natural_stride);
348
349    as_strided(array, &output_shape, &combined_strides)
350}
351
352/// Returns the byte strides of an array.
353///
354/// Byte strides represent the number of bytes to move along each dimension
355/// when navigating the array in memory.
356///
357/// # Arguments
358///
359/// * `array` - The input array
360///
361/// # Returns
362///
363/// A vector containing the byte strides for each dimension of the array
364///
365/// # Examples
366///
367/// ```
368/// use numrs2::prelude::*;
369///
370/// let array = Array::from_vec(vec![1, 2, 3, 4, 5, 6]).reshape(&[2, 3]);
371/// let strides = byte_strides(&array);
372/// ```
373pub fn byte_strides<T>(array: &Array<T>) -> Vec<usize>
374where
375    T: Clone + Debug,
376{
377    // Get the memory strides in terms of elements
378    let elem_strides = array.array().strides();
379
380    // Convert to byte strides by multiplying by the size of T
381    let elem_size = std::mem::size_of::<T>();
382    elem_strides
383        .iter()
384        .map(|&s| s as usize * elem_size)
385        .collect()
386}
387
388/// Create views into arrays in a way that broadcasting might occur.
389///
390/// This function is similar to NumPy's `broadcast_arrays`, but uses
391/// stride manipulation to create the views.
392///
393/// # Arguments
394///
395/// * `arrays` - A slice of arrays to broadcast together
396///
397/// # Returns
398///
399/// * `Ok(Vec<Array<T>>)` - A vector of arrays that are broadcast to have the same shape
400/// * `Err(NumRs2Error)` - Error if arrays cannot be broadcast together
401///
402/// # Examples
403///
404/// ```
405/// use numrs2::prelude::*;
406///
407/// let a = Array::from_vec(vec![1, 2, 3]).reshape(&[1, 3]);
408/// let b = Array::from_vec(vec![4, 5, 6]).reshape(&[3, 1]);
409///
410/// let result = broadcast_arrays(&[&a, &b]).expect("broadcast_arrays should succeed");
411/// assert_eq!(result.len(), 2);
412/// assert_eq!(result[0].shape(), result[1].shape());
413/// assert_eq!(result[0].to_vec(), vec![1, 2, 3, 1, 2, 3, 1, 2, 3]);
414/// assert_eq!(result[1].to_vec(), vec![4, 4, 4, 5, 5, 5, 6, 6, 6]);
415/// ```
416pub fn broadcast_arrays<T>(arrays: &[&Array<T>]) -> Result<Vec<Array<T>>>
417where
418    T: Clone + Debug,
419{
420    if arrays.is_empty() {
421        return Ok(Vec::new());
422    }
423
424    // Get the shapes of all arrays
425    let shapes: Vec<_> = arrays.iter().map(|a| a.shape()).collect();
426
427    // Determine the output shape (the shape all arrays will be broadcast to)
428    let output_shape = broadcast_shape(&shapes)?;
429
430    // Broadcast each array to the output shape
431    let mut result = Vec::with_capacity(arrays.len());
432    for array in arrays {
433        let broadcast = broadcast_to(array, &output_shape)?;
434        result.push(broadcast);
435    }
436
437    Ok(result)
438}
439
440/// Broadcast an array to a new shape using stride tricks.
441///
442/// This function is similar to NumPy's `broadcast_to`, but uses
443/// stride manipulation to create the view.
444///
445/// # Arguments
446///
447/// * `array` - The input array to broadcast
448/// * `shape` - The target shape to broadcast to
449///
450/// # Returns
451///
452/// * `Ok(Array<T>)` - The broadcast array
453/// * `Err(NumRs2Error)` - Error if the array cannot be broadcast to the target shape
454///
455/// # Examples
456///
457/// ```
458/// use numrs2::prelude::*;
459///
460/// let array = Array::from_vec(vec![1, 2, 3]).reshape(&[1, 3]);
461///
462/// // Broadcast to shape [3, 3]
463/// let result = broadcast_to(&array, &[3, 3]).expect("broadcast_to should succeed");
464/// assert_eq!(result.shape(), vec![3, 3]);
465/// assert_eq!(result.to_vec(), vec![1, 2, 3, 1, 2, 3, 1, 2, 3]);
466/// ```
467pub fn broadcast_to<T>(array: &Array<T>, shape: &[usize]) -> Result<Array<T>>
468where
469    T: Clone + Debug,
470{
471    // Check if the array can be broadcast to the target shape
472    if !is_broadcastable(&array.shape(), shape) {
473        return Err(NumRs2Error::ShapeMismatch {
474            expected: shape.to_vec(),
475            actual: array.shape(),
476        });
477    }
478
479    // Get the original shape and its natural (element) strides. `as_strided`
480    // interprets strides as element offsets into the row-major flattened
481    // buffer (see its docs), so these must be element strides -- not the
482    // byte strides `byte_strides()` returns.
483    let orig_shape = array.shape();
484    let elem_strides = row_major_strides(&orig_shape);
485
486    // Calculate the new strides for the broadcast array
487    let mut new_strides = Vec::with_capacity(shape.len());
488
489    // Prepend dimensions to match the length of the target shape
490    let prepend_dims = shape.len() - orig_shape.len();
491    new_strides.extend(std::iter::repeat_n(0, prepend_dims)); // Stride 0 for broadcast dimensions
492
493    // Set strides for existing dimensions
494    for (i, &dim) in orig_shape.iter().enumerate() {
495        let target_dim = shape[i + prepend_dims];
496        if dim == 1 && target_dim > 1 {
497            // Broadcasting from a dimension of size 1 to a larger size
498            new_strides.push(0);
499        } else {
500            // Keep original stride for non-broadcast dimensions
501            new_strides.push(elem_strides[i]);
502        }
503    }
504
505    // Use as_strided to create the broadcast view
506    as_strided(array, shape, &new_strides)
507}
508
509/// Check if an array shape can be broadcast to a target shape.
510///
511/// Broadcasting rules:
512/// 1. If the two arrays have different numbers of dimensions, prepend the shape
513///    of the one with fewer dimensions with 1s until both shapes have the same length.
514/// 2. The size in each dimension of the output shape is the maximum of the sizes
515///    of the two input arrays in that dimension.
516/// 3. An array can be broadcast along a dimension if its size in that dimension is 1
517///    or if it doesn't have that dimension.
518///
519/// # Arguments
520///
521/// * `source_shape` - The shape of the source array
522/// * `target_shape` - The shape to broadcast to
523///
524/// # Returns
525///
526/// True if the source shape can be broadcast to the target shape, false otherwise
527fn is_broadcastable(source_shape: &[usize], target_shape: &[usize]) -> bool {
528    // A scalar can be broadcast to any shape
529    if source_shape.is_empty() {
530        return true;
531    }
532
533    // If the source has more dimensions than target, it cannot be broadcast
534    if source_shape.len() > target_shape.len() {
535        return false;
536    }
537
538    // Check each dimension from the end (right-aligned)
539    let offset = target_shape.len() - source_shape.len();
540    for (i, &dim) in source_shape.iter().enumerate() {
541        let target_dim = target_shape[i + offset];
542        if dim != 1 && dim != target_dim {
543            return false;
544        }
545    }
546
547    true
548}
549
550/// Determine the output shape when broadcasting arrays together.
551///
552/// # Arguments
553///
554/// * `shapes` - A slice of array shapes to broadcast together
555///
556/// # Returns
557///
558/// * `Ok(Vec<usize>)` - The broadcast shape
559/// * `Err(NumRs2Error)` - Error if shapes cannot be broadcast together
560fn broadcast_shape(shapes: &[Vec<usize>]) -> Result<Vec<usize>> {
561    if shapes.is_empty() {
562        return Ok(Vec::new());
563    }
564
565    // Find the maximum number of dimensions
566    // Safe: shapes is non-empty (checked above), so max() returns Some
567    let max_ndim = shapes.iter().map(|s| s.len()).max().unwrap_or(0);
568
569    // Initialize the output shape with 1s
570    let mut output_shape = vec![1; max_ndim];
571
572    // Determine the output shape
573    for shape in shapes {
574        let offset = max_ndim - shape.len();
575        for (i, &dim) in shape.iter().enumerate() {
576            let out_i = i + offset;
577            if output_shape[out_i] == 1 {
578                output_shape[out_i] = dim;
579            } else if dim != 1 && dim != output_shape[out_i] {
580                return Err(NumRs2Error::InvalidOperation(
581                    format!("Incompatible shapes for broadcasting: dimension {} has conflicting sizes {} and {}",
582                            out_i, output_shape[out_i], dim)
583                ));
584            }
585        }
586    }
587
588    Ok(output_shape)
589}