Skip to main content

ry_physics/
lib.rs

1//! RyDit Physics - Módulo de Física para RyDit
2//!
3//! Proporciona funcionalidad de:
4//! - Proyectil 2D (trayectoria, altura máxima, alcance)
5//! - Gravedad N-cuerpos (2 cuerpos)
6
7use ry_core::{ModuleError, ModuleResult, RyditModule};
8use serde_json::{json, Value};
9use std::collections::HashMap;
10
11/// Módulo de Física - Proyectil y Gravedad
12pub struct PhysicsModule;
13
14impl RyditModule for PhysicsModule {
15    fn name(&self) -> &'static str {
16        "physics"
17    }
18
19    fn version(&self) -> &'static str {
20        "0.7.3"
21    }
22
23    fn register(&self) -> HashMap<&'static str, &'static str> {
24        let mut cmds = HashMap::new();
25        cmds.insert("projectile", "Simulación de proyectil");
26        cmds.insert("nbody_2", "Simulación N-cuerpos (2 cuerpos)");
27        cmds.insert("nbody_simulate", "Simulación N-cuerpos (múltiples cuerpos)");
28        cmds
29    }
30
31    fn execute(&self, command: &str, params: Value) -> ModuleResult {
32        match command {
33            "projectile" => self.projectile(params),
34            "nbody_2" => self.nbody_2(params),
35            "nbody_simulate" => self.nbody_simulate(params),
36            _ => Err(ModuleError {
37                code: "UNKNOWN_COMMAND".to_string(),
38                message: format!("Comando desconocido: {}", command),
39            }),
40        }
41    }
42}
43
44impl PhysicsModule {
45    /// Simulación de proyectil 2D
46    ///
47    /// # Params
48    /// - x0, y0: Posición inicial
49    /// - v0: Velocidad inicial (m/s)
50    /// - angle: Ángulo en grados
51    ///
52    /// # Returns
53    /// [x_final, y_final, flight_time, max_height, range]
54    fn projectile(&self, params: Value) -> ModuleResult {
55        let arr = params.as_array().ok_or_else(|| ModuleError {
56            code: "INVALID_PARAMS".to_string(),
57            message: "Params must be an array".to_string(),
58        })?;
59
60        if arr.len() != 4 {
61            return Err(ModuleError {
62                code: "INVALID_PARAMS".to_string(),
63                message: "physics::projectile requires 4 params: x0, y0, v0, angle".to_string(),
64            });
65        }
66
67        let x0 = arr[0].as_f64().unwrap_or(0.0);
68        let y0 = arr[1].as_f64().unwrap_or(0.0);
69        let v0 = arr[2].as_f64().unwrap_or(0.0);
70        let angle = arr[3].as_f64().unwrap_or(0.0);
71
72        let rad = angle.to_radians();
73        let vx = v0 * rad.cos();
74        let vy = v0 * rad.sin();
75        let g = 9.81;
76
77        let flight_time = 2.0 * vy / g;
78        let max_height = (vy * vy) / (2.0 * g);
79        let range = vx * flight_time;
80
81        Ok(json!([
82            x0 + vx * flight_time, // x final
83            y0,                    // y final
84            flight_time,           // tiempo vuelo
85            max_height,            // altura máxima
86            range                  // alcance horizontal
87        ]))
88    }
89
90    /// Gravedad entre 2 cuerpos (Ley de Newton)
91    ///
92    /// # Params
93    /// - m1, m2: Masas de los cuerpos
94    /// - x1, y1: Posición del cuerpo 1
95    /// - x2, y2: Posición del cuerpo 2
96    /// - G: Constante gravitacional (default: 6.674e-11)
97    ///
98    /// # Returns
99    /// [fx1, fy1, fx2, fy2, distancia]
100    fn nbody_2(&self, params: Value) -> ModuleResult {
101        let arr = params.as_array().ok_or_else(|| ModuleError {
102            code: "INVALID_PARAMS".to_string(),
103            message: "Params must be an array".to_string(),
104        })?;
105
106        if arr.len() != 7 {
107            return Err(ModuleError {
108                code: "INVALID_PARAMS".to_string(),
109                message: "physics::nbody_2 requires 7 params: m1, m2, x1, y1, x2, y2, G"
110                    .to_string(),
111            });
112        }
113
114        let m1 = arr[0].as_f64().unwrap_or(0.0);
115        let m2 = arr[1].as_f64().unwrap_or(0.0);
116        let x1 = arr[2].as_f64().unwrap_or(0.0);
117        let y1 = arr[3].as_f64().unwrap_or(0.0);
118        let x2 = arr[4].as_f64().unwrap_or(0.0);
119        let y2 = arr[5].as_f64().unwrap_or(0.0);
120        let g = arr[6].as_f64().unwrap_or(6.674e-11);
121
122        let dx = x2 - x1;
123        let dy = y2 - y1;
124        let dist = (dx * dx + dy * dy).sqrt();
125
126        if dist > 0.001 {
127            let force = g * m1 * m2 / (dist * dist);
128            let fx = force * dx / dist;
129            let fy = force * dy / dist;
130
131            Ok(json!([fx, fy, -fx, -fy, dist]))
132        } else {
133            Ok(json!([0.0, 0.0, 0.0, 0.0, dist]))
134        }
135    }
136
137    // ========================================================================
138    // N-BODY SIMULATION (N cuerpos, O(n²)) — v0.13.1
139    // ========================================================================
140
141    /// Simulación N-cuerpos con gravedad mutua
142    ///
143    /// # Params
144    /// - bodies: array de [mass, x, y, vx, vy, is_static] por cuerpo
145    /// - dt: delta tiempo
146    /// - G: constante gravitacional (default: 6.674e-11)
147    ///
148    /// # Returns
149    /// Array de [x, y, vx, vy] actualizado por cuerpo
150    pub fn nbody_simulate(&self, params: Value) -> ModuleResult {
151        let arr = params.as_array().ok_or_else(|| ModuleError {
152            code: "INVALID_PARAMS".to_string(),
153            message: "nbody_simulate requiere array de bodies + dt + G".to_string(),
154        })?;
155
156        if arr.len() < 3 {
157            return Err(ModuleError {
158                code: "INVALID_PARAMS".to_string(),
159                message: "nbody_simulate requiere [bodies, dt, G]".to_string(),
160            });
161        }
162
163        let bodies_arr = arr[0].as_array().ok_or_else(|| ModuleError {
164            code: "INVALID_PARAMS".to_string(),
165            message: "bodies debe ser array de [mass, x, y, vx, vy, is_static]".to_string(),
166        })?;
167
168        let dt = arr[1].as_f64().unwrap_or(0.016);
169        let g = arr[2].as_f64().unwrap_or(6.674e-11);
170
171        let n = bodies_arr.len();
172        if n == 0 {
173            return Ok(json!([]));
174        }
175
176        // Parse bodies: [mass, x, y, vx, vy, is_static]
177        struct Body {
178            mass: f64, x: f64, y: f64, vx: f64, vy: f64, is_static: bool,
179            ax: f64, ay: f64,
180        }
181
182        let mut bodies: Vec<Body> = bodies_arr.iter().filter_map(|b| {
183            let a = b.as_array()?;
184            if a.len() < 6 { return None; }
185            Some(Body {
186                mass: a[0].as_f64()?,
187                x: a[1].as_f64()?,
188                y: a[2].as_f64()?,
189                vx: a[3].as_f64()?,
190                vy: a[4].as_f64()?,
191                is_static: a[5].as_f64().map(|v| v != 0.0).unwrap_or(false),
192                ax: 0.0, ay: 0.0,
193            })
194        }).collect();
195
196        // Calculate gravitational accelerations: O(n²)
197        for i in 0..n {
198            for j in (i + 1)..n {
199                let dx = bodies[j].x - bodies[i].x;
200                let dy = bodies[j].y - bodies[i].y;
201                let dist_sq = dx * dx + dy * dy;
202                let dist = dist_sq.sqrt();
203                if dist < 0.001 { continue; }
204                let force = g * bodies[i].mass * bodies[j].mass / dist_sq;
205                let ax = force * dx / (dist * bodies[i].mass);
206                let ay = force * dy / (dist * bodies[i].mass);
207                let ax_j = -force * dx / (dist * bodies[j].mass);
208                let ay_j = -force * dy / (dist * bodies[j].mass);
209                if !bodies[i].is_static { bodies[i].ax += ax; bodies[i].ay += ay; }
210                if !bodies[j].is_static { bodies[j].ax += ax_j; bodies[j].ay += ay_j; }
211            }
212        }
213
214        // Update positions and velocities
215        let result: Vec<Value> = bodies.iter().map(|b| {
216            let vx = if b.is_static { b.vx } else { b.vx + b.ax * dt };
217            let vy = if b.is_static { b.vy } else { b.vy + b.ay * dt };
218            let x = if b.is_static { b.x } else { b.x + vx * dt };
219            let y = if b.is_static { b.y } else { b.y + vy * dt };
220            json!([x, y, vx, vy])
221        }).collect();
222
223        Ok(json!(result))
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_physics_module_name() {
233        let module = PhysicsModule;
234        assert_eq!(module.name(), "physics");
235        assert_eq!(module.version(), "0.7.3");
236    }
237
238    #[test]
239    fn test_physics_register() {
240        let module = PhysicsModule;
241        let cmds = module.register();
242
243        assert!(cmds.contains_key("projectile"));
244        assert!(cmds.contains_key("nbody_2"));
245    }
246
247    #[test]
248    fn test_projectile() {
249        let module = PhysicsModule;
250        // x0=0, y0=0, v0=10, angle=45
251        let params = json!([0.0, 0.0, 10.0, 45.0]);
252        let result = module.execute("projectile", params).unwrap();
253
254        let arr = result.as_array().unwrap();
255        assert_eq!(arr.len(), 5);
256        // flight_time ≈ 2*vy/g = 2*(10*sin(45))/9.81 ≈ 1.44s
257        let flight_time = arr[2].as_f64().unwrap();
258        assert!(flight_time > 1.4 && flight_time < 1.5);
259    }
260
261    #[test]
262    fn test_nbody_2() {
263        let module = PhysicsModule;
264        // m1=100, m2=200, x1=0, y1=0, x2=10, y2=0, G=1.0
265        let params = json!([100.0, 200.0, 0.0, 0.0, 10.0, 0.0, 1.0]);
266        let result = module.execute("nbody_2", params).unwrap();
267
268        let arr = result.as_array().unwrap();
269        assert_eq!(arr.len(), 5);
270        // fx = G*m1*m2/dist^2 * dx/dist = 1*100*200/100 * 10/10 = 200
271        let fx = arr[0].as_f64().unwrap();
272        assert!((fx - 200.0).abs() < 0.01);
273    }
274
275    #[test]
276    fn test_nbody_2_close() {
277        let module = PhysicsModule;
278        // Cuerpos muy cercanos (dist < 0.001)
279        let params = json!([100.0, 200.0, 0.0, 0.0, 0.0001, 0.0, 1.0]);
280        let result = module.execute("nbody_2", params).unwrap();
281
282        let arr = result.as_array().unwrap();
283        assert_eq!(arr[0].as_f64().unwrap(), 0.0);
284        assert_eq!(arr[1].as_f64().unwrap(), 0.0);
285    }
286
287    #[test]
288    fn test_unknown_command() {
289        let module = PhysicsModule;
290        let result = module.execute("unknown", json!([]));
291
292        assert!(result.is_err());
293        let err = result.unwrap_err();
294        assert_eq!(err.code, "UNKNOWN_COMMAND");
295    }
296}