1#![allow(dead_code)]
7
8use crate::{bessel, erf, gamma, statistical};
9use std::collections::HashMap;
10
11pub struct MigrationGuide {
13 mappings: HashMap<String, FunctionMapping>,
14 reverse_mappings: HashMap<String, String>, }
16
17#[derive(Clone)]
19pub struct FunctionMapping {
20 pub scipy_name: String,
21 pub scirs2_name: String,
22 pub module_path: String,
23 pub signature_changes: Vec<String>,
24 pub notes: Vec<String>,
25}
26
27impl Default for MigrationGuide {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl MigrationGuide {
34 pub fn new() -> Self {
36 let mut mappings = HashMap::new();
37 let mut reverse_mappings = HashMap::new();
38
39 mappings.insert(
41 "scipy.special.gamma".to_string(),
42 FunctionMapping {
43 scipy_name: "gamma".to_string(),
44 scirs2_name: "gamma".to_string(),
45 module_path: "scirs2_special::gamma".to_string(),
46 signature_changes: vec![],
47 notes: vec![
48 "Direct replacement, same signature".to_string(),
49 "Returns NaN for negative integers (poles)".to_string(),
50 ],
51 },
52 );
53
54 mappings.insert(
55 "scipy.special.gammaln".to_string(),
56 FunctionMapping {
57 scipy_name: "gammaln".to_string(),
58 scirs2_name: "gammaln".to_string(),
59 module_path: "scirs2_special::gamma".to_string(),
60 signature_changes: vec![],
61 notes: vec!["Direct replacement".to_string()],
62 },
63 );
64
65 mappings.insert(
66 "scipy.special.beta".to_string(),
67 FunctionMapping {
68 scipy_name: "beta".to_string(),
69 scirs2_name: "beta".to_string(),
70 module_path: "scirs2_special::gamma".to_string(),
71 signature_changes: vec![],
72 notes: vec!["Direct replacement".to_string()],
73 },
74 );
75
76 mappings.insert(
78 "scipy.special.j0".to_string(),
79 FunctionMapping {
80 scipy_name: "j0".to_string(),
81 scirs2_name: "j0".to_string(),
82 module_path: "scirs2_special::bessel".to_string(),
83 signature_changes: vec![],
84 notes: vec!["Direct replacement".to_string()],
85 },
86 );
87
88 mappings.insert(
89 "scipy.special.jv".to_string(),
90 FunctionMapping {
91 scipy_name: "jv".to_string(),
92 scirs2_name: "jv".to_string(),
93 module_path: "scirs2_special::bessel".to_string(),
94 signature_changes: vec!["Order parameter v must implement Float trait".to_string()],
95 notes: vec!["Supports both integer and fractional orders".to_string()],
96 },
97 );
98
99 mappings.insert(
101 "scipy.special.erf".to_string(),
102 FunctionMapping {
103 scipy_name: "erf".to_string(),
104 scirs2_name: "erf".to_string(),
105 module_path: "scirs2_special::erf".to_string(),
106 signature_changes: vec![],
107 notes: vec![
108 "Direct replacement".to_string(),
109 "Complex version available as erf_complex".to_string(),
110 ],
111 },
112 );
113
114 mappings.insert(
115 "scipy.special.erfc".to_string(),
116 FunctionMapping {
117 scipy_name: "erfc".to_string(),
118 scirs2_name: "erfc".to_string(),
119 module_path: "scirs2_special::erf".to_string(),
120 signature_changes: vec![],
121 notes: vec!["Direct replacement".to_string()],
122 },
123 );
124
125 mappings.insert(
127 "scipy.special.expit".to_string(),
128 FunctionMapping {
129 scipy_name: "expit".to_string(),
130 scirs2_name: "logistic".to_string(),
131 module_path: "scirs2_special::statistical".to_string(),
132 signature_changes: vec![],
133 notes: vec![
134 "Name change: expit -> logistic".to_string(),
135 "Same mathematical function: 1/(1+exp(-x))".to_string(),
136 ],
137 },
138 );
139
140 mappings.insert(
141 "scipy.special.softmax".to_string(),
142 FunctionMapping {
143 scipy_name: "softmax".to_string(),
144 scirs2_name: "softmax".to_string(),
145 module_path: "scirs2_special::statistical".to_string(),
146 signature_changes: vec![
147 "Takes &[f64] slice instead of numpy array".to_string(),
148 "Returns Vec<f64> instead of numpy array".to_string(),
149 ],
150 notes: vec!["Use ndarray for array operations".to_string()],
151 },
152 );
153
154 mappings.insert(
156 "scipy.special.legendre".to_string(),
157 FunctionMapping {
158 scipy_name: "legendre".to_string(),
159 scirs2_name: "legendre_p".to_string(),
160 module_path: "scirs2_special::orthogonal".to_string(),
161 signature_changes: vec![
162 "Returns polynomial value, not polynomial object".to_string(),
163 "Use legendre_p(n, x) for evaluation".to_string(),
164 ],
165 notes: vec![
166 "SciPy returns polynomial object, SciRS2 evaluates directly".to_string()
167 ],
168 },
169 );
170
171 mappings.insert(
175 "scipy.special.airy".to_string(),
176 FunctionMapping {
177 scipy_name: "airy".to_string(),
178 scirs2_name: "ai, bi, aip, bip".to_string(),
179 module_path: "scirs2_special::airy".to_string(),
180 signature_changes: vec![
181 "SciPy returns tuple (Ai, Aip, Bi, Bip)".to_string(),
182 "SciRS2 has separate functions for each".to_string(),
183 ],
184 notes: vec!["Use individual functions: ai(x), bi(x), aip(x), bip(x)".to_string()],
185 },
186 );
187
188 mappings.insert(
190 "scipy.special.ellipk".to_string(),
191 FunctionMapping {
192 scipy_name: "ellipk".to_string(),
193 scirs2_name: "elliptic_k".to_string(),
194 module_path: "scirs2_special::elliptic".to_string(),
195 signature_changes: vec![],
196 notes: vec!["Name change: ellipk -> elliptic_k".to_string()],
197 },
198 );
199
200 mappings.insert(
202 "scipy.special.hyp1f1".to_string(),
203 FunctionMapping {
204 scipy_name: "hyp1f1".to_string(),
205 scirs2_name: "hyp1f1".to_string(),
206 module_path: "scirs2_special::hypergeometric".to_string(),
207 signature_changes: vec![],
208 notes: vec!["Direct replacement".to_string()],
209 },
210 );
211
212 mappings.insert(
214 "scipy.special.sph_harm".to_string(),
215 FunctionMapping {
216 scipy_name: "sph_harm".to_string(),
217 scirs2_name: "sph_harm_complex".to_string(),
218 module_path: "scirs2_special::spherical_harmonics".to_string(),
219 signature_changes: vec![
220 "Parameter order: (m, n, theta, phi) in SciPy".to_string(),
221 "Parameter order: (n, m, theta, phi) in SciRS2".to_string(),
222 ],
223 notes: vec!["Watch out for parameter order change".to_string()],
224 },
225 );
226
227 for (scipy_name, mapping) in &mappings {
229 reverse_mappings.insert(mapping.scirs2_name.clone(), scipy_name.clone());
230 }
231
232 MigrationGuide {
233 mappings,
234 reverse_mappings,
235 }
236 }
237
238 pub fn get_mapping(&self, scipyfunc: &str) -> Option<&FunctionMapping> {
240 self.mappings.get(scipyfunc)
241 }
242
243 pub fn get_reverse_mapping(&self, scirs2func: &str) -> Option<&String> {
245 self.reverse_mappings.get(scirs2func)
246 }
247
248 pub fn list_all_mappings(&self) -> Vec<(&String, &FunctionMapping)> {
250 self.mappings.iter().collect()
251 }
252
253 pub fn generate_migration_report(&self, scipyfunctions: &[&str]) -> String {
255 let mut report = String::from("SciPy to SciRS2 Migration Report\n");
256 report.push_str("================================\n\n");
257
258 for &func in scipyfunctions {
259 if let Some(mapping) = self.get_mapping(func) {
260 report.push_str(&format!("## {func}\n"));
261 report.push_str(&format!("SciRS2 equivalent: `{}`\n", mapping.module_path));
262
263 if !mapping.signature_changes.is_empty() {
264 report.push_str("\nSignature changes:\n");
265 for change in &mapping.signature_changes {
266 report.push_str(&format!("- {change}\n"));
267 }
268 }
269
270 if !mapping.notes.is_empty() {
271 report.push_str("\nNotes:\n");
272 for note in &mapping.notes {
273 report.push_str(&format!("- {note}\n"));
274 }
275 }
276
277 report.push('\n');
278 } else {
279 report.push_str(&format!("## {func}\n"));
280 report.push_str(
281 "⚠️ No direct mapping found. May require custom implementation.\n\n",
282 );
283 }
284 }
285
286 report
287 }
288}
289
290pub mod compat {
292 use super::*;
293 use scirs2_core::ndarray::{Array1, ArrayView1};
294
295 pub fn gamma_array(x: &ArrayView1<f64>) -> Array1<f64> {
297 x.mapv(gamma::gamma)
298 }
299
300 pub fn erf_array(x: &ArrayView1<f64>) -> Array1<f64> {
302 x.mapv(erf::erf)
303 }
304
305 pub fn j0_array(x: &ArrayView1<f64>) -> Array1<f64> {
307 x.mapv(bessel::j0)
308 }
309
310 pub fn softmax_axis(x: &ArrayView1<f64>, _axis: Option<usize>) -> Vec<f64> {
312 match statistical::softmax(x.view()) {
315 Ok(result) => result.to_vec(),
316 Err(_) => vec![],
317 }
318 }
319}
320
321pub mod codegen {
323 use super::*;
324 #[cfg(feature = "python-interop")]
325 use regex::Regex;
326
327 pub fn generate_rust_equivalent(_scipycode: &str) -> Result<String, String> {
329 #[cfg(feature = "python-interop")]
330 {
331 generate_rust_equivalent_regex(_scipycode)
332 }
333
334 #[cfg(not(feature = "python-interop"))]
335 {
336 generate_rust_equivalent_simple(_scipycode)
337 }
338 }
339
340 #[cfg(feature = "python-interop")]
341 fn generate_rust_equivalent_regex(_scipycode: &str) -> Result<String, String> {
342 let guide = MigrationGuide::new();
343 let mut rust_code = String::new();
344 let mut imports = std::collections::HashSet::new();
345 let mut code_lines = Vec::new();
346
347 let patterns = vec![
349 (r"scipy\.special\.(\w+)\s*\(", "scipy.special."),
350 (r"from scipy\.special import (\w+(?:,\s*\w+)*)", ""),
351 (r"special\.(\w+)\s*\(", "scipy.special."),
352 ];
353
354 let mut found_functions = Vec::new();
356 for (pattern, prefix) in patterns {
357 let re = Regex::new(pattern).map_err(|e| e.to_string())?;
358 for cap in re.captures_iter(_scipycode) {
359 if let Some(func_match) = cap.get(1) {
360 let funcs = func_match.as_str();
361 for func in funcs.split(',') {
362 let func = func.trim();
363 let full_name = format!("{prefix}{func}");
364 if let Some(mapping) = guide.get_mapping(&full_name) {
365 found_functions.push((func.to_string(), mapping.clone()));
366 imports.insert(mapping.module_path.clone());
367 }
368 }
369 }
370 }
371 }
372
373 for import in &imports {
375 rust_code.push_str(&format!("use {import};\n"));
376 }
377
378 if !imports.is_empty() {
379 rust_code.push('\n');
380 }
381
382 let mut transformed = _scipycode.to_string();
384 for (scipyfunc, mapping) in &found_functions {
385 code_lines.push(format!("// {} -> {}", scipyfunc, mapping.scirs2_name));
387
388 transformed =
390 transformed.replace(&format!("scipy.special.{scipyfunc}"), &mapping.scirs2_name);
391 transformed =
392 transformed.replace(&format!("special.{scipyfunc}"), &mapping.scirs2_name);
393 }
394
395 if !found_functions.is_empty() {
397 rust_code.push_str("// Transformed _code:\n");
398 rust_code.push_str(&format!("// {transformed}\n"));
399
400 rust_code.push_str("\n// Notes:\n");
401 for (_, mapping) in &found_functions {
402 for note in &mapping.notes {
403 rust_code.push_str(&format!("// - {note}\n"));
404 }
405 }
406 }
407
408 if rust_code.is_empty() {
409 Err("No recognized SciPy functions found".to_string())
410 } else {
411 Ok(rust_code)
412 }
413 }
414
415 fn generate_rust_equivalent_simple(_scipycode: &str) -> Result<String, String> {
416 let guide = MigrationGuide::new();
417 let mut rust_code = String::new();
418
419 let known_functions = vec!["gamma", "erf", "j0", "j1", "beta", "gammaln"];
421
422 for func in known_functions {
423 let scipy_pattern = format!("scipy.special.{func}");
424 if _scipycode.contains(&scipy_pattern) {
425 let full_name = format!("scipy.special.{func}");
426 if let Some(mapping) = guide.get_mapping(&full_name) {
427 let module_path = &mapping.module_path;
428 rust_code.push_str(&format!("use {module_path};\n"));
429 let scirs2_name = &mapping.scirs2_name;
430 rust_code.push_str(&format!("// Replace {scipy_pattern} with {scirs2_name}\n"));
431 }
432 }
433 }
434
435 if rust_code.is_empty() {
436 Err("No recognized SciPy functions found".to_string())
437 } else {
438 Ok(rust_code)
439 }
440 }
441
442 pub fn generate_imports(_scipyimports: &[&str]) -> String {
444 let mut _imports = String::from("// SciRS2 _imports\n");
445
446 for &import in _scipyimports {
447 match import {
448 "gamma" | "gammaln" | "beta" => {
449 _imports.push_str("use scirs2_special::gamma::{gamma, gammaln, beta};\n");
450 }
451 "j0" | "j1" | "jv" => {
452 _imports.push_str("use scirs2_special::bessel::{j0, j1, jv};\n");
453 }
454 "erf" | "erfc" => {
455 _imports.push_str("use scirs2_special::erf::{erf, erfc};\n");
456 }
457 _ => {}
458 }
459 }
460
461 _imports
462 }
463}
464
465pub mod performance {
467 pub struct PerformanceComparison {
469 pub function_name: String,
470 pub scipy_time_ms: f64,
471 pub scirs2_time_ms: f64,
472 pub speedup: f64,
473 pub accuracy_difference: f64,
474 }
475
476 impl PerformanceComparison {
477 pub fn report(&self) -> String {
479 format!(
480 "{}: SciRS2 is {:.1}x {} (accuracy diff: {:.2e})",
481 self.function_name,
482 if self.speedup > 1.0 {
483 self.speedup
484 } else {
485 1.0 / self.speedup
486 },
487 if self.speedup > 1.0 {
488 "faster"
489 } else {
490 "slower"
491 },
492 self.accuracy_difference
493 )
494 }
495 }
496}
497
498pub mod examples {
500 pub fn gamma_migration_example() -> String {
502 r#"
503// SciPy Python code:
504// from scipy.special import gamma
505// result = gamma(5.5)
506
507// SciRS2 Rust equivalent:
508use scirs2_special::gamma;
509
510let result = gamma(5.5_f64);
511
512// For array operations:
513use scirs2_core::ndarray::Array1;
514
515let x = Array1::linspace(0.1, 10.0, 100);
516let gamma_values = x.mapv(gamma);
517"#
518 .to_string()
519 }
520
521 pub fn bessel_migration_example() -> String {
523 r#"
524// SciPy Python code:
525// from scipy.special import j0, jv
526// y1 = j0(2.5)
527// y2 = jv(1.5, 3.0)
528
529// SciRS2 Rust equivalent:
530use scirs2_special::bessel::{j0, jv};
531
532let y1 = j0(2.5_f64);
533let y2 = jv(1.5_f64, 3.0_f64);
534"#
535 .to_string()
536 }
537
538 pub fn statistical_migration_example() -> String {
540 r#"
541// SciPy Python code:
542// from scipy.special import expit, softmax
543// sigmoid = expit(x)
544// probs = softmax(logits)
545
546// SciRS2 Rust equivalent:
547use scirs2_special::statistical::{logistic, softmax};
548
549let sigmoid = logistic(x);
550let probs = softmax(&logits); // Note: takes a slice
551
552// For ndarray:
553use scirs2_core::ndarray::Array1;
554
555let x_array = Array1::from_vec(vec![1.0, 2.0, 3.0]);
556let sigmoid_array = x_array.mapv(logistic);
557"#
558 .to_string()
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
567 fn test_migration_guide() {
568 let guide = MigrationGuide::new();
569
570 let gamma_mapping = guide
572 .get_mapping("scipy.special.gamma")
573 .expect("Operation failed");
574 assert_eq!(gamma_mapping.scirs2_name, "gamma");
575
576 let expit_mapping = guide
577 .get_mapping("scipy.special.expit")
578 .expect("Operation failed");
579 assert_eq!(expit_mapping.scirs2_name, "logistic");
580 }
581
582 #[test]
583 fn test_migration_report() {
584 let guide = MigrationGuide::new();
585 let functions = vec![
586 "scipy.special.gamma",
587 "scipy.special.erf",
588 "scipy.special.unknown",
589 ];
590 let report = guide.generate_migration_report(&functions);
591
592 assert!(report.contains("gamma"));
593 assert!(report.contains("erf"));
594 assert!(report.contains("No direct mapping found"));
595 }
596
597 #[test]
598 fn test_codegen() {
599 let _scipycode = "result = scipy.special.gamma(x)";
600 let rust_code = codegen::generate_rust_equivalent(_scipycode).expect("Operation failed");
601
602 assert!(rust_code.contains("use scirs2_special::gamma"));
603 }
604
605 #[test]
606 fn test_performance_comparison() {
607 let comparison = performance::PerformanceComparison {
608 function_name: "gamma".to_string(),
609 scipy_time_ms: 10.0,
610 scirs2_time_ms: 2.0,
611 speedup: 5.0,
612 accuracy_difference: 1e-15,
613 };
614
615 let report = comparison.report();
616 assert!(report.contains("5.0x faster"));
617 }
618}