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}