Skip to main content

ry_science/
lib.rs

1//! RyDit Science - Módulo de Ciencia para RyDit
2//!
3//! Proporciona funcionalidad de:
4//! - Curvas Bezier (lineal, cuadrática, cúbica)
5//! - Estadísticas (media, mediana, mínimo, máximo)
6//! - Geometría (ilusiones ópticas: Penrose, Cubo imposible, Espiral)
7
8pub mod geometry;
9
10use ry_core::{ModuleError, ModuleResult, RyditModule};
11use serde_json::{json, Value};
12use std::collections::HashMap;
13
14/// Módulo de Ciencia - Bezier y Estadísticas
15pub struct ScienceModule;
16
17impl RyditModule for ScienceModule {
18    fn name(&self) -> &'static str {
19        "science"
20    }
21
22    fn version(&self) -> &'static str {
23        "0.7.3"
24    }
25
26    fn register(&self) -> HashMap<&'static str, &'static str> {
27        let mut cmds = HashMap::new();
28        cmds.insert("bezier::linear", "Curva Bezier lineal");
29        cmds.insert("bezier::quadratic", "Curva Bezier cuadrática");
30        cmds.insert("bezier::cubic", "Curva Bezier cúbica");
31        cmds.insert("stats::mean", "Media aritmética");
32        cmds.insert("stats::median", "Mediana");
33        cmds.insert("stats::min", "Valor mínimo");
34        cmds.insert("stats::max", "Valor máximo");
35        cmds.insert("geometry::penrose", "Triángulo de Penrose");
36        cmds.insert("geometry::impossible_cube", "Cubo imposible");
37        cmds.insert("geometry::spiral", "Espiral óptica");
38        cmds.insert("geometry::muller_lyer", "Ilusión Müller-Lyer");
39        cmds.insert("geometry::ponzo", "Ilusión de Ponzo");
40        cmds
41    }
42
43    fn execute(&self, command: &str, params: Value) -> ModuleResult {
44        match command {
45            "bezier::linear" => self.bezier_linear(params),
46            "bezier::quadratic" => self.bezier_quadratic(params),
47            "bezier::cubic" => self.bezier_cubic(params),
48            "stats::mean" => self.stats_mean(params),
49            "stats::median" => self.stats_median(params),
50            "stats::min" => self.stats_min(params),
51            "stats::max" => self.stats_max(params),
52            "geometry::penrose" => self.geometry_penrose(params),
53            "geometry::impossible_cube" => self.geometry_impossible_cube(params),
54            "geometry::spiral" => self.geometry_spiral(params),
55            "geometry::muller_lyer" => self.geometry_muller_lyer(params),
56            "geometry::ponzo" => self.geometry_ponzo(params),
57            _ => Err(ModuleError {
58                code: "UNKNOWN_COMMAND".to_string(),
59                message: format!("Comando desconocido: {}", command),
60            }),
61        }
62    }
63}
64
65impl ScienceModule {
66    /// Curva Bezier lineal: P(t) = (1-t)*P0 + t*P1
67    fn bezier_linear(&self, params: Value) -> ModuleResult {
68        let arr = params.as_array().ok_or_else(|| ModuleError {
69            code: "INVALID_PARAMS".to_string(),
70            message: "Params must be an array".to_string(),
71        })?;
72
73        if arr.len() != 5 {
74            return Err(ModuleError {
75                code: "INVALID_PARAMS".to_string(),
76                message: "bezier::linear requires 5 params: p0_x, p0_y, p1_x, p1_y, t".to_string(),
77            });
78        }
79
80        let p0_x = arr[0].as_f64().unwrap_or(0.0);
81        let p0_y = arr[1].as_f64().unwrap_or(0.0);
82        let p1_x = arr[2].as_f64().unwrap_or(0.0);
83        let p1_y = arr[3].as_f64().unwrap_or(0.0);
84        let t = arr[4].as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
85
86        let x = (1.0 - t) * p0_x + t * p1_x;
87        let y = (1.0 - t) * p0_y + t * p1_y;
88
89        Ok(json!([x, y]))
90    }
91
92    /// Curva Bezier cuadrática: P(t) = (1-t)²*P0 + 2(1-t)t*P1 + t²*P2
93    fn bezier_quadratic(&self, params: Value) -> ModuleResult {
94        let arr = params.as_array().ok_or_else(|| ModuleError {
95            code: "INVALID_PARAMS".to_string(),
96            message: "Params must be an array".to_string(),
97        })?;
98
99        if arr.len() != 7 {
100            return Err(ModuleError {
101                code: "INVALID_PARAMS".to_string(),
102                message:
103                    "bezier::quadratic requires 7 params: p0_x, p0_y, p1_x, p1_y, p2_x, p2_y, t"
104                        .to_string(),
105            });
106        }
107
108        let p0_x = arr[0].as_f64().unwrap_or(0.0);
109        let p0_y = arr[1].as_f64().unwrap_or(0.0);
110        let p1_x = arr[2].as_f64().unwrap_or(0.0);
111        let p1_y = arr[3].as_f64().unwrap_or(0.0);
112        let p2_x = arr[4].as_f64().unwrap_or(0.0);
113        let p2_y = arr[5].as_f64().unwrap_or(0.0);
114        let t = arr[6].as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
115
116        let mt = 1.0 - t;
117        let x = mt * mt * p0_x + 2.0 * mt * t * p1_x + t * t * p2_x;
118        let y = mt * mt * p0_y + 2.0 * mt * t * p1_y + t * t * p2_y;
119
120        Ok(json!([x, y]))
121    }
122
123    /// Curva Bezier cúbica: P(t) = (1-t)³*P0 + 3(1-t)²t*P1 + 3(1-t)t²*P2 + t³*P3
124    fn bezier_cubic(&self, params: Value) -> ModuleResult {
125        let arr = params.as_array().ok_or_else(|| ModuleError {
126            code: "INVALID_PARAMS".to_string(),
127            message: "Params must be an array".to_string(),
128        })?;
129
130        if arr.len() != 9 {
131            return Err(ModuleError {
132                code: "INVALID_PARAMS".to_string(),
133                message: "bezier::cubic requires 9 params: p0_x, p0_y, p1_x, p1_y, p2_x, p2_y, p3_x, p3_y, t".to_string(),
134            });
135        }
136
137        let p0_x = arr[0].as_f64().unwrap_or(0.0);
138        let p0_y = arr[1].as_f64().unwrap_or(0.0);
139        let p1_x = arr[2].as_f64().unwrap_or(0.0);
140        let p1_y = arr[3].as_f64().unwrap_or(0.0);
141        let p2_x = arr[4].as_f64().unwrap_or(0.0);
142        let p2_y = arr[5].as_f64().unwrap_or(0.0);
143        let p3_x = arr[6].as_f64().unwrap_or(0.0);
144        let p3_y = arr[7].as_f64().unwrap_or(0.0);
145        let t = arr[8].as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
146
147        let mt = 1.0 - t;
148        let mt2 = mt * mt;
149        let t2 = t * t;
150
151        let x = mt2 * mt * p0_x + 3.0 * mt2 * t * p1_x + 3.0 * mt * t2 * p2_x + t2 * t * p3_x;
152        let y = mt2 * mt * p0_y + 3.0 * mt2 * t * p1_y + 3.0 * mt * t2 * p2_y + t2 * t * p3_y;
153
154        Ok(json!([x, y]))
155    }
156
157    /// Media aritmética: sum / n
158    fn stats_mean(&self, params: Value) -> ModuleResult {
159        let arr = params.as_array().ok_or_else(|| ModuleError {
160            code: "INVALID_PARAMS".to_string(),
161            message: "Params must be an array".to_string(),
162        })?;
163
164        if arr.is_empty() {
165            return Err(ModuleError {
166                code: "INVALID_PARAMS".to_string(),
167                message: "Empty array".to_string(),
168            });
169        }
170
171        let sum: f64 = arr.iter().filter_map(|v| v.as_f64()).sum();
172        Ok(json!(sum / arr.len() as f64))
173    }
174
175    /// Mediana: valor central de array ordenado
176    fn stats_median(&self, params: Value) -> ModuleResult {
177        let arr = params.as_array().ok_or_else(|| ModuleError {
178            code: "INVALID_PARAMS".to_string(),
179            message: "Params must be an array".to_string(),
180        })?;
181
182        let mut nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
183
184        if nums.is_empty() {
185            return Err(ModuleError {
186                code: "INVALID_PARAMS".to_string(),
187                message: "Empty array or no numbers".to_string(),
188            });
189        }
190
191        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
192        let mid = nums.len() / 2;
193
194        let median = if nums.len().is_multiple_of(2) {
195            (nums[mid - 1] + nums[mid]) / 2.0
196        } else {
197            nums[mid]
198        };
199
200        Ok(json!(median))
201    }
202
203    /// Valor mínimo de un array
204    fn stats_min(&self, params: Value) -> ModuleResult {
205        let arr = params.as_array().ok_or_else(|| ModuleError {
206            code: "INVALID_PARAMS".to_string(),
207            message: "Params must be an array".to_string(),
208        })?;
209
210        let mut min_val = f64::MAX;
211        let mut found = false;
212
213        for v in arr {
214            if let Some(n) = v.as_f64() {
215                if n < min_val {
216                    min_val = n;
217                }
218                found = true;
219            }
220        }
221
222        if found {
223            Ok(json!(min_val))
224        } else {
225            Err(ModuleError {
226                code: "INVALID_PARAMS".to_string(),
227                message: "No numbers in array".to_string(),
228            })
229        }
230    }
231
232    /// Valor máximo de un array
233    fn stats_max(&self, params: Value) -> ModuleResult {
234        let arr = params.as_array().ok_or_else(|| ModuleError {
235            code: "INVALID_PARAMS".to_string(),
236            message: "Params must be an array".to_string(),
237        })?;
238
239        let mut max_val = f64::MIN;
240        let mut found = false;
241
242        for v in arr {
243            if let Some(n) = v.as_f64() {
244                if n > max_val {
245                    max_val = n;
246                }
247                found = true;
248            }
249        }
250
251        if found {
252            Ok(json!(max_val))
253        } else {
254            Err(ModuleError {
255                code: "INVALID_PARAMS".to_string(),
256                message: "No numbers in array".to_string(),
257            })
258        }
259    }
260
261    // ================================================================
262    // Funciones de Geometría - Ilusiones Ópticas
263    // ================================================================
264
265    /// Triángulo de Penrose (tribar imposible)
266    fn geometry_penrose(&self, params: Value) -> ModuleResult {
267        let arr = params.as_array().ok_or_else(|| ModuleError {
268            code: "INVALID_PARAMS".to_string(),
269            message: "Params must be an array".to_string(),
270        })?;
271
272        if arr.len() != 3 {
273            return Err(ModuleError {
274                code: "INVALID_PARAMS".to_string(),
275                message: "geometry::penrose requires 3 params: center_x, center_y, size"
276                    .to_string(),
277            });
278        }
279
280        let center_x = arr[0].as_f64().unwrap_or(400.0);
281        let center_y = arr[1].as_f64().unwrap_or(300.0);
282        let size = arr[2].as_f64().unwrap_or(100.0);
283
284        Ok(geometry::penrose(center_x, center_y, size))
285    }
286
287    /// Cubo imposible (Necker cube)
288    fn geometry_impossible_cube(&self, params: Value) -> ModuleResult {
289        let arr = params.as_array().ok_or_else(|| ModuleError {
290            code: "INVALID_PARAMS".to_string(),
291            message: "Params must be an array".to_string(),
292        })?;
293
294        if arr.len() != 3 {
295            return Err(ModuleError {
296                code: "INVALID_PARAMS".to_string(),
297                message: "geometry::impossible_cube requires 3 params: center_x, center_y, size"
298                    .to_string(),
299            });
300        }
301
302        let center_x = arr[0].as_f64().unwrap_or(400.0);
303        let center_y = arr[1].as_f64().unwrap_or(300.0);
304        let size = arr[2].as_f64().unwrap_or(100.0);
305
306        Ok(geometry::impossible_cube(center_x, center_y, size))
307    }
308
309    /// Espiral óptica (Arquímedes)
310    fn geometry_spiral(&self, params: Value) -> ModuleResult {
311        let arr = params.as_array().ok_or_else(|| ModuleError {
312            code: "INVALID_PARAMS".to_string(),
313            message: "Params must be an array".to_string(),
314        })?;
315
316        if arr.len() != 5 {
317            return Err(ModuleError {
318                code: "INVALID_PARAMS".to_string(),
319                message:
320                    "geometry::spiral requires 5 params: center_x, center_y, turns, radius, points"
321                        .to_string(),
322            });
323        }
324
325        let center_x = arr[0].as_f64().unwrap_or(400.0);
326        let center_y = arr[1].as_f64().unwrap_or(300.0);
327        let turns = arr[2].as_i64().unwrap_or(3) as i32;
328        let radius = arr[3].as_f64().unwrap_or(100.0);
329        let points = arr[4].as_i64().unwrap_or(20) as i32;
330
331        Ok(geometry::spiral(center_x, center_y, turns, radius, points))
332    }
333
334    /// Ilusión de Müller-Lyer (flechas)
335    fn geometry_muller_lyer(&self, params: Value) -> ModuleResult {
336        let arr = params.as_array().ok_or_else(|| ModuleError {
337            code: "INVALID_PARAMS".to_string(),
338            message: "Params must be an array".to_string(),
339        })?;
340
341        if arr.len() != 3 {
342            return Err(ModuleError {
343                code: "INVALID_PARAMS".to_string(),
344                message: "geometry::muller_lyer requires 3 params: center_x, center_y, length"
345                    .to_string(),
346            });
347        }
348
349        let center_x = arr[0].as_f64().unwrap_or(400.0);
350        let center_y = arr[1].as_f64().unwrap_or(300.0);
351        let length = arr[2].as_f64().unwrap_or(200.0);
352
353        Ok(geometry::muller_lyer(center_x, center_y, length))
354    }
355
356    /// Ilusión de Ponzo (perspectiva)
357    fn geometry_ponzo(&self, params: Value) -> ModuleResult {
358        let arr = params.as_array().ok_or_else(|| ModuleError {
359            code: "INVALID_PARAMS".to_string(),
360            message: "Params must be an array".to_string(),
361        })?;
362
363        if arr.len() != 5 {
364            return Err(ModuleError {
365                code: "INVALID_PARAMS".to_string(),
366                message: "geometry::ponzo requires 5 params: center_x, center_y, height, width_top, width_bottom".to_string(),
367            });
368        }
369
370        let center_x = arr[0].as_f64().unwrap_or(400.0);
371        let center_y = arr[1].as_f64().unwrap_or(300.0);
372        let height = arr[2].as_f64().unwrap_or(300.0);
373        let width_top = arr[3].as_f64().unwrap_or(100.0);
374        let width_bottom = arr[4].as_f64().unwrap_or(300.0);
375
376        Ok(geometry::ponzo(
377            center_x,
378            center_y,
379            height,
380            width_top,
381            width_bottom,
382        ))
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_science_module_name() {
392        let module = ScienceModule;
393        assert_eq!(module.name(), "science");
394        assert_eq!(module.version(), "0.7.3");
395    }
396
397    #[test]
398    fn test_science_register() {
399        let module = ScienceModule;
400        let cmds = module.register();
401
402        assert!(cmds.contains_key("bezier::linear"));
403        assert!(cmds.contains_key("bezier::cubic"));
404        assert!(cmds.contains_key("stats::mean"));
405        assert!(cmds.contains_key("stats::median"));
406        assert!(cmds.contains_key("geometry::penrose"));
407        assert!(cmds.contains_key("geometry::impossible_cube"));
408        assert!(cmds.contains_key("geometry::spiral"));
409    }
410
411    #[test]
412    fn test_bezier_linear() {
413        let module = ScienceModule;
414        let params = json!([0.0, 0.0, 100.0, 100.0, 0.5]);
415        let result = module.execute("bezier::linear", params).unwrap();
416
417        assert_eq!(result, json!([50.0, 50.0]));
418    }
419
420    #[test]
421    fn test_bezier_cubic() {
422        let module = ScienceModule;
423        // p0=(0,0), p1=(30,100), p2=(70,100), p3=(100,0), t=0.5
424        let params = json!([0.0, 0.0, 30.0, 100.0, 70.0, 100.0, 100.0, 0.0, 0.5]);
425        let result = module.execute("bezier::cubic", params).unwrap();
426
427        assert_eq!(result, json!([50.0, 75.0]));
428    }
429
430    #[test]
431    fn test_stats_mean() {
432        let module = ScienceModule;
433        let params = json!([1.0, 2.0, 3.0, 4.0, 5.0]);
434        let result = module.execute("stats::mean", params).unwrap();
435
436        assert_eq!(result, json!(3.0));
437    }
438
439    #[test]
440    fn test_stats_median_odd() {
441        let module = ScienceModule;
442        let params = json!([1.0, 2.0, 3.0, 4.0, 5.0]);
443        let result = module.execute("stats::median", params).unwrap();
444
445        assert_eq!(result, json!(3.0));
446    }
447
448    #[test]
449    fn test_stats_median_even() {
450        let module = ScienceModule;
451        let params = json!([1.0, 2.0, 3.0, 4.0]);
452        let result = module.execute("stats::median", params).unwrap();
453
454        assert_eq!(result, json!(2.5));
455    }
456
457    #[test]
458    fn test_stats_min_max() {
459        let module = ScienceModule;
460        let params = json!([3.0, 1.0, 4.0, 1.0, 5.0]);
461
462        let min_result = module.execute("stats::min", params.clone()).unwrap();
463        assert_eq!(min_result, json!(1.0));
464
465        let max_result = module.execute("stats::max", params).unwrap();
466        assert_eq!(max_result, json!(5.0));
467    }
468
469    #[test]
470    fn test_unknown_command() {
471        let module = ScienceModule;
472        let result = module.execute("unknown", json!([]));
473
474        assert!(result.is_err());
475        let err = result.unwrap_err();
476        assert_eq!(err.code, "UNKNOWN_COMMAND");
477    }
478
479    // Tests de Geometría
480    #[test]
481    fn test_geometry_penrose() {
482        let module = ScienceModule;
483        let params = json!([400.0, 300.0, 100.0]);
484        let result = module.execute("geometry::penrose", params).unwrap();
485
486        let lines = result.as_array().unwrap();
487        assert!(!lines.is_empty());
488        assert!(lines.len() >= 10);
489    }
490
491    #[test]
492    fn test_geometry_impossible_cube() {
493        let module = ScienceModule;
494        let params = json!([400.0, 300.0, 100.0]);
495        let result = module.execute("geometry::impossible_cube", params).unwrap();
496
497        let lines = result.as_array().unwrap();
498        assert!(!lines.is_empty());
499        assert!(lines.len() >= 12);
500    }
501
502    #[test]
503    fn test_geometry_spiral() {
504        let module = ScienceModule;
505        let params = json!([400.0, 300.0, 3, 100.0, 20]);
506        let result = module.execute("geometry::spiral", params).unwrap();
507
508        let points = result.as_array().unwrap();
509        assert_eq!(points.len(), 60); // 3 turns * 20 points
510    }
511
512    #[test]
513    fn test_geometry_muller_lyer() {
514        let module = ScienceModule;
515        let params = json!([400.0, 300.0, 200.0]);
516        let result = module.execute("geometry::muller_lyer", params).unwrap();
517
518        let lines = result.as_array().unwrap();
519        assert_eq!(lines.len(), 10);
520    }
521
522    #[test]
523    fn test_geometry_ponzo() {
524        let module = ScienceModule;
525        let params = json!([400.0, 300.0, 300.0, 100.0, 300.0]);
526        let result = module.execute("geometry::ponzo", params).unwrap();
527
528        let lines = result.as_array().unwrap();
529        assert_eq!(lines.len(), 6); // 2 rieles + 4 horizontales
530    }
531}