Skip to main content

scirs2_core/scipy_migration/
numpy_equiv.rs

1//! NumPy -> SciRS2 / ndarray equivalence reference
2//!
3//! This module provides a searchable mapping from NumPy function names
4//! to their SciRS2 or ndarray equivalents. NumPy's core array type
5//! is represented by `ndarray::Array` (re-exported via `scirs2_core::ndarray`).
6//!
7//! # Example
8//!
9//! ```rust
10//! use scirs2_core::scipy_migration::numpy_equiv::{search_numpy, numpy_table};
11//!
12//! let hits = search_numpy("zeros");
13//! assert!(!hits.is_empty());
14//! ```
15
16/// A single NumPy equivalence entry.
17#[derive(Debug, Clone)]
18pub struct NumpyEntry {
19    /// NumPy function path (e.g., `"numpy.zeros"`)
20    pub numpy_path: &'static str,
21    /// Rust equivalent (ndarray or scirs2 path)
22    pub rust_path: &'static str,
23    /// Notes about differences
24    pub notes: &'static str,
25    /// NumPy usage example (Python)
26    pub numpy_example: &'static str,
27    /// Rust usage example
28    pub rust_example: &'static str,
29}
30
31/// Returns the full NumPy equivalence table.
32pub fn numpy_table() -> &'static [NumpyEntry] {
33    &NUMPY_TABLE
34}
35
36/// Search for a NumPy function by name (case-insensitive partial match).
37pub fn search_numpy(query: &str) -> Vec<&'static NumpyEntry> {
38    let lower = query.to_lowercase();
39    NUMPY_TABLE
40        .iter()
41        .filter(|e| e.numpy_path.to_lowercase().contains(&lower))
42        .collect()
43}
44
45// ---------------------------------------------------------------------------
46// NumPy Equivalence Table
47// ---------------------------------------------------------------------------
48
49static NUMPY_TABLE: [NumpyEntry; 42] = [
50    // ===== Array Creation =====
51    NumpyEntry {
52        numpy_path: "numpy.array",
53        rust_path: "ndarray::array! or ndarray::Array::from_vec",
54        notes: "Use the array![] macro for literals, or Array::from_vec / from_shape_vec for dynamic data.",
55        numpy_example: r#"import numpy as np
56a = np.array([1.0, 2.0, 3.0])
57b = np.array([[1, 2], [3, 4]])"#,
58        rust_example: r#"use ndarray::{array, Array1, Array2};
59let a = array![1.0, 2.0, 3.0];
60let b = array![[1.0, 2.0], [3.0, 4.0]];"#,
61    },
62    NumpyEntry {
63        numpy_path: "numpy.zeros",
64        rust_path: "ndarray::Array::zeros",
65        notes: "Create an array filled with zeros.",
66        numpy_example: r#"a = np.zeros((3, 4))"#,
67        rust_example: r#"use ndarray::Array2;
68let a = Array2::<f64>::zeros((3, 4));"#,
69    },
70    NumpyEntry {
71        numpy_path: "numpy.ones",
72        rust_path: "ndarray::Array::ones",
73        notes: "Create an array filled with ones.",
74        numpy_example: r#"a = np.ones((3, 4))"#,
75        rust_example: r#"use ndarray::Array2;
76let a = Array2::<f64>::ones((3, 4));"#,
77    },
78    NumpyEntry {
79        numpy_path: "numpy.eye",
80        rust_path: "ndarray::Array2::eye",
81        notes: "Identity matrix.",
82        numpy_example: r#"I = np.eye(3)"#,
83        rust_example: r#"use ndarray::Array2;
84let eye = Array2::<f64>::eye(3);"#,
85    },
86    NumpyEntry {
87        numpy_path: "numpy.full",
88        rust_path: "ndarray::Array::from_elem",
89        notes: "Create an array filled with a given value.",
90        numpy_example: r#"a = np.full((3, 4), 7.0)"#,
91        rust_example: r#"use ndarray::Array2;
92let a = Array2::<f64>::from_elem((3, 4), 7.0);"#,
93    },
94    NumpyEntry {
95        numpy_path: "numpy.arange",
96        rust_path: "ndarray::Array::range or ndarray::Array::linspace",
97        notes: "arange with step: use Array::range(start, end, step).",
98        numpy_example: r#"a = np.arange(0, 10, 0.5)"#,
99        rust_example: r#"use ndarray::Array1;
100let a = Array1::range(0.0, 10.0, 0.5);"#,
101    },
102    NumpyEntry {
103        numpy_path: "numpy.linspace",
104        rust_path: "ndarray::Array::linspace",
105        notes: "Evenly spaced values over an interval.",
106        numpy_example: r#"a = np.linspace(0, 1, 50)"#,
107        rust_example: r#"use ndarray::Array1;
108let a = Array1::linspace(0.0, 1.0, 50);"#,
109    },
110    NumpyEntry {
111        numpy_path: "numpy.empty",
112        rust_path: "ndarray::Array::uninit or Array::zeros",
113        notes: "Rust does not have uninitialized arrays in safe code. Use zeros() or from_elem().",
114        numpy_example: r#"a = np.empty((3, 4))"#,
115        rust_example: r#"use ndarray::Array2;
116// No direct equivalent; use zeros instead:
117let a = Array2::<f64>::zeros((3, 4));"#,
118    },
119    // ===== Shape Manipulation =====
120    NumpyEntry {
121        numpy_path: "numpy.reshape",
122        rust_path: "ndarray::Array::into_shape_with_order",
123        notes: "Reshape an array. The new shape must have the same total number of elements.",
124        numpy_example: r#"b = a.reshape((2, 3))"#,
125        rust_example: r#"let b = a.into_shape_with_order((2, 3))?;"#,
126    },
127    NumpyEntry {
128        numpy_path: "numpy.transpose",
129        rust_path: "ndarray::Array::t() or .reversed_axes()",
130        notes: "Transpose. .t() returns a view; .reversed_axes() consumes the array.",
131        numpy_example: r#"b = a.T
132b = np.transpose(a)"#,
133        rust_example: r#"let b = a.t();  // transposed view
134let b = a.reversed_axes();  // consumes a"#,
135    },
136    NumpyEntry {
137        numpy_path: "numpy.concatenate",
138        rust_path: "ndarray::concatenate or ndarray::stack",
139        notes: "Join arrays along an existing axis. Use ndarray::concatenate![] macro.",
140        numpy_example: r#"c = np.concatenate([a, b], axis=0)"#,
141        rust_example: r#"use ndarray::concatenate;
142use ndarray::Axis;
143let c = concatenate(Axis(0), &[a.view(), b.view()])?;"#,
144    },
145    NumpyEntry {
146        numpy_path: "numpy.stack",
147        rust_path: "ndarray::stack",
148        notes: "Stack arrays along a new axis.",
149        numpy_example: r#"c = np.stack([a, b], axis=0)"#,
150        rust_example: r#"use ndarray::{stack, Axis};
151let c = stack(Axis(0), &[a.view(), b.view()])?;"#,
152    },
153    NumpyEntry {
154        numpy_path: "numpy.split",
155        rust_path: "ndarray slicing",
156        notes: "No direct split function; use array slicing with s![] macro.",
157        numpy_example: r#"parts = np.split(a, 3, axis=0)"#,
158        rust_example: r#"use ndarray::s;
159let part0 = a.slice(s![0..n, ..]);
160let part1 = a.slice(s![n..2*n, ..]);"#,
161    },
162    NumpyEntry {
163        numpy_path: "numpy.flatten",
164        rust_path: "ndarray::Array::into_raw_vec or .iter()",
165        notes: "Flatten to 1-D. Use .into_raw_vec() for owned, or .iter() for iteration.",
166        numpy_example: r#"flat = a.flatten()"#,
167        rust_example: r#"let flat: Vec<f64> = a.into_raw_vec();
168// or iterate:
169let flat: Array1<f64> = a.iter().cloned().collect();"#,
170    },
171    NumpyEntry {
172        numpy_path: "numpy.squeeze",
173        rust_path: "ndarray: remove_axis",
174        notes: "Remove size-1 axes. Use .index_axis(Axis(n), 0) to remove a specific axis.",
175        numpy_example: r#"b = np.squeeze(a)"#,
176        rust_example: r#"use ndarray::Axis;
177let b = a.index_axis(Axis(0), 0);  // remove axis 0 if size==1"#,
178    },
179    // ===== Math Operations =====
180    NumpyEntry {
181        numpy_path: "numpy.dot",
182        rust_path: "ndarray .dot() method",
183        notes: "Matrix/vector dot product. Array2.dot(&Array2), Array1.dot(&Array1).",
184        numpy_example: r#"c = np.dot(a, b)"#,
185        rust_example: r#"let c = a.dot(&b);"#,
186    },
187    NumpyEntry {
188        numpy_path: "numpy.matmul",
189        rust_path: "ndarray .dot() method",
190        notes: "Same as np.dot for 2-D arrays. For batched, see scirs2_linalg::batch_matmul.",
191        numpy_example: r#"c = np.matmul(a, b)
192c = a @ b"#,
193        rust_example: r#"let c = a.dot(&b);
194// Batched: use scirs2_linalg::prelude::batch_matmul"#,
195    },
196    NumpyEntry {
197        numpy_path: "numpy.sum",
198        rust_path: "ndarray .sum() or .sum_axis()",
199        notes: "Sum all elements or along an axis.",
200        numpy_example: r#"s = np.sum(a)
201s = np.sum(a, axis=0)"#,
202        rust_example: r#"let s = a.sum();
203let s = a.sum_axis(ndarray::Axis(0));"#,
204    },
205    NumpyEntry {
206        numpy_path: "numpy.mean",
207        rust_path: "ndarray .mean() or scirs2_stats::mean",
208        notes: "Mean value. ndarray's .mean() returns Option; scirs2_stats::mean returns Result.",
209        numpy_example: r#"m = np.mean(a)"#,
210        rust_example: r#"let m = a.mean();  // returns Option<f64>
211// or:
212use scirs2_stats::mean;
213let m = mean(&a.view())?;"#,
214    },
215    NumpyEntry {
216        numpy_path: "numpy.var",
217        rust_path: "ndarray .var(ddof) or scirs2_stats::var",
218        notes: "Variance. ndarray uses .var(ddof); scirs2_stats::var(x, ddof, workers).",
219        numpy_example: r#"v = np.var(a, ddof=1)"#,
220        rust_example: r#"let v = a.var(1.0);
221// or:
222use scirs2_stats::var;
223let v = var(&a.view(), 1, None)?;"#,
224    },
225    NumpyEntry {
226        numpy_path: "numpy.std",
227        rust_path: "ndarray .std(ddof) or scirs2_stats::std",
228        notes: "Standard deviation.",
229        numpy_example: r#"s = np.std(a, ddof=1)"#,
230        rust_example: r#"let s = a.std(1.0);
231// or:
232use scirs2_stats::std;
233let s = std(&a.view(), 1, None)?;"#,
234    },
235    NumpyEntry {
236        numpy_path: "numpy.max / numpy.min",
237        rust_path: "ndarray: a.iter().cloned().fold() or a.fold()",
238        notes: "No built-in max/min on ndarray; iterate or use scirs2_core utilities.",
239        numpy_example: r#"mx = np.max(a)
240mn = np.min(a)"#,
241        rust_example: r#"let mx = a.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
242let mn = a.iter().cloned().fold(f64::INFINITY, f64::min);"#,
243    },
244    NumpyEntry {
245        numpy_path: "numpy.abs",
246        rust_path: "ndarray .mapv(f64::abs)",
247        notes: "Element-wise absolute value.",
248        numpy_example: r#"b = np.abs(a)"#,
249        rust_example: r#"let b = a.mapv(f64::abs);"#,
250    },
251    NumpyEntry {
252        numpy_path: "numpy.sqrt",
253        rust_path: "ndarray .mapv(f64::sqrt)",
254        notes: "Element-wise square root.",
255        numpy_example: r#"b = np.sqrt(a)"#,
256        rust_example: r#"let b = a.mapv(f64::sqrt);"#,
257    },
258    NumpyEntry {
259        numpy_path: "numpy.exp",
260        rust_path: "ndarray .mapv(f64::exp)",
261        notes: "Element-wise exponential.",
262        numpy_example: r#"b = np.exp(a)"#,
263        rust_example: r#"let b = a.mapv(f64::exp);"#,
264    },
265    NumpyEntry {
266        numpy_path: "numpy.log",
267        rust_path: "ndarray .mapv(f64::ln)",
268        notes: "Element-wise natural logarithm. Note: Rust uses ln(), not log().",
269        numpy_example: r#"b = np.log(a)"#,
270        rust_example: r#"let b = a.mapv(f64::ln);"#,
271    },
272    NumpyEntry {
273        numpy_path: "numpy.sin / numpy.cos / numpy.tan",
274        rust_path: "ndarray .mapv(f64::sin) / .mapv(f64::cos) / .mapv(f64::tan)",
275        notes: "Element-wise trigonometric functions.",
276        numpy_example: r#"b = np.sin(a)"#,
277        rust_example: r#"let b = a.mapv(f64::sin);"#,
278    },
279    NumpyEntry {
280        numpy_path: "numpy.clip",
281        rust_path: "ndarray .mapv(|x| x.clamp(lo, hi))",
282        notes: "Clip values to a range.",
283        numpy_example: r#"b = np.clip(a, 0, 1)"#,
284        rust_example: r#"let b = a.mapv(|x: f64| x.clamp(0.0, 1.0));"#,
285    },
286    NumpyEntry {
287        numpy_path: "numpy.where",
288        rust_path: "ndarray .mapv() with conditional or Zip",
289        notes: "Conditional element selection. Use mapv or ndarray::Zip for element-wise conditions.",
290        numpy_example: r#"b = np.where(a > 0, a, 0)"#,
291        rust_example: r#"let b = a.mapv(|x: f64| if x > 0.0 { x } else { 0.0 });"#,
292    },
293    // ===== Linear Algebra =====
294    NumpyEntry {
295        numpy_path: "numpy.linalg.det",
296        rust_path: "scirs2_linalg::prelude::det",
297        notes: "Matrix determinant.",
298        numpy_example: r#"d = np.linalg.det(a)"#,
299        rust_example: r#"use scirs2_linalg::prelude::det;
300let d = det(&a.view())?;"#,
301    },
302    NumpyEntry {
303        numpy_path: "numpy.linalg.inv",
304        rust_path: "scirs2_linalg::prelude::inv",
305        notes: "Matrix inverse.",
306        numpy_example: r#"a_inv = np.linalg.inv(a)"#,
307        rust_example: r#"use scirs2_linalg::prelude::inv;
308let a_inv = inv(&a.view())?;"#,
309    },
310    NumpyEntry {
311        numpy_path: "numpy.linalg.solve",
312        rust_path: "scirs2_linalg::prelude::solve",
313        notes: "Solve Ax = b.",
314        numpy_example: r#"x = np.linalg.solve(a, b)"#,
315        rust_example: r#"use scirs2_linalg::prelude::solve;
316let x = solve(&a.view(), &b.view())?;"#,
317    },
318    NumpyEntry {
319        numpy_path: "numpy.linalg.eig",
320        rust_path: "scirs2_linalg::prelude::eig",
321        notes: "Eigenvalue decomposition.",
322        numpy_example: r#"vals, vecs = np.linalg.eig(a)"#,
323        rust_example: r#"use scirs2_linalg::prelude::eig;
324let (vals, vecs) = eig(&a.view())?;"#,
325    },
326    NumpyEntry {
327        numpy_path: "numpy.linalg.svd",
328        rust_path: "scirs2_linalg::prelude::svd",
329        notes: "Singular Value Decomposition.",
330        numpy_example: r#"U, s, Vt = np.linalg.svd(a)"#,
331        rust_example: r#"use scirs2_linalg::prelude::svd;
332let (u, s, vt) = svd(&a.view())?;"#,
333    },
334    NumpyEntry {
335        numpy_path: "numpy.linalg.norm",
336        rust_path: "scirs2_linalg::prelude::vector_norm / matrix_norm",
337        notes: "Vector or matrix norm. Use vector_norm for 1-D, matrix_norm for 2-D.",
338        numpy_example: r#"n = np.linalg.norm(a)"#,
339        rust_example: r#"use scirs2_linalg::prelude::{vector_norm, matrix_norm};
340let n = vector_norm(&v.view(), 2.0)?;
341let n = matrix_norm(&m.view(), "fro")?;"#,
342    },
343    NumpyEntry {
344        numpy_path: "numpy.linalg.lstsq",
345        rust_path: "scirs2_linalg::prelude::lstsq",
346        notes: "Least-squares solution.",
347        numpy_example: r#"x, res, rank, sv = np.linalg.lstsq(a, b, rcond=None)"#,
348        rust_example: r#"use scirs2_linalg::prelude::lstsq;
349let result = lstsq(&a.view(), &b.view())?;"#,
350    },
351    // ===== Indexing & Slicing =====
352    NumpyEntry {
353        numpy_path: "numpy indexing: a[i, j]",
354        rust_path: "ndarray: a[[i, j]] or a.get([i, j])",
355        notes: "Direct indexing uses [[i, j]]. .get() returns Option for bounds checking.",
356        numpy_example: r#"val = a[2, 3]"#,
357        rust_example: r#"let val = a[[2, 3]];
358// Safe version:
359let val = a.get([2, 3]);"#,
360    },
361    NumpyEntry {
362        numpy_path: "numpy slicing: a[1:3, :]",
363        rust_path: "ndarray: a.slice(s![1..3, ..])",
364        notes: "Use the s![] macro for slicing. Supports ranges, steps, and negative indices.",
365        numpy_example: r#"b = a[1:3, :]
366c = a[::2, :]"#,
367        rust_example: r#"use ndarray::s;
368let b = a.slice(s![1..3, ..]);
369let c = a.slice(s![..;2, ..]);"#,
370    },
371    NumpyEntry {
372        numpy_path: "numpy boolean indexing: a[a > 0]",
373        rust_path: "ndarray: filtering with iterators",
374        notes: "No direct boolean indexing; use .iter().filter() or .mapv() with conditions.",
375        numpy_example: r#"b = a[a > 0]"#,
376        rust_example: r#"let b: Vec<f64> = a.iter().filter(|&&x| x > 0.0).cloned().collect();"#,
377    },
378    // ===== Random =====
379    NumpyEntry {
380        numpy_path: "numpy.random.rand",
381        rust_path: "scirs2_core::random",
382        notes: "Uniform [0, 1) random array. Use scirs2_core random utilities.",
383        numpy_example: r#"a = np.random.rand(3, 4)"#,
384        rust_example: r#"use scirs2_core::random;
385// Generate uniform random values using scirs2_core random module"#,
386    },
387    NumpyEntry {
388        numpy_path: "numpy.random.randn",
389        rust_path: "scirs2_core::random",
390        notes: "Standard normal random array.",
391        numpy_example: r#"a = np.random.randn(3, 4)"#,
392        rust_example: r#"use scirs2_core::random;
393// Generate normal random values using scirs2_core random module"#,
394    },
395    NumpyEntry {
396        numpy_path: "numpy.random.seed",
397        rust_path: "scirs2_core::random (seed-based RNG)",
398        notes: "Use seeded RNG from scirs2_core::random for reproducibility.",
399        numpy_example: r#"np.random.seed(42)"#,
400        rust_example: r#"use scirs2_core::random;
401// Create seeded RNG for reproducible results"#,
402    },
403];
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn test_numpy_equiv_table_not_empty() {
411        assert!(!numpy_table().is_empty());
412        assert_eq!(numpy_table().len(), 42);
413    }
414
415    #[test]
416    fn test_search_numpy() {
417        let results = search_numpy("zeros");
418        assert!(!results.is_empty());
419        assert!(results.iter().any(|e| e.numpy_path == "numpy.zeros"));
420    }
421
422    #[test]
423    fn test_search_numpy_case_insensitive() {
424        let upper = search_numpy("ZEROS");
425        let lower = search_numpy("zeros");
426        assert_eq!(upper.len(), lower.len());
427    }
428
429    #[test]
430    fn test_search_numpy_linalg() {
431        let results = search_numpy("linalg");
432        assert!(results.len() >= 5);
433    }
434
435    #[test]
436    fn test_all_rust_paths_non_empty() {
437        for entry in numpy_table() {
438            assert!(
439                !entry.rust_path.is_empty(),
440                "Empty rust_path for {}",
441                entry.numpy_path
442            );
443        }
444    }
445
446    #[test]
447    fn test_no_duplicate_numpy_paths() {
448        let table = numpy_table();
449        let mut seen = std::collections::HashSet::new();
450        for entry in table {
451            assert!(
452                seen.insert(entry.numpy_path),
453                "Duplicate numpy_path: {}",
454                entry.numpy_path
455            );
456        }
457    }
458}