scirs2_integrate/ode/
mechanical.rs

1//! Mechanical systems integration methods
2//!
3//! This module provides specialized numerical integration methods for mechanical systems,
4//! including rigid body dynamics, multibody systems with constraints, and deformable solids.
5//! These methods are optimized for the specific structure and properties of mechanical systems.
6
7use crate::error::{IntegrateError, IntegrateResult};
8use scirs2_core::ndarray::{s, Array1, Array2};
9
10/// Type alias for external force functions
11type ExternalForceFunction =
12    Box<dyn Fn(f64, &Array1<f64>, &Array1<f64>) -> Array1<f64> + Send + Sync>;
13
14/// Type alias for constraint functions
15type ConstraintFunction = Box<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
16
17/// Type alias for constraint Jacobian functions
18type ConstraintJacobianFunction = Box<dyn Fn(&Array1<f64>) -> Array2<f64> + Send + Sync>;
19
20/// Type alias for integration result tuples
21type IntegrationResult = Result<(Array1<f64>, f64, usize, bool), Box<dyn std::error::Error>>;
22
23/// Types of mechanical systems supported
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum MechanicalSystemType {
26    /// Single rigid body with 6 DOF
27    RigidBody,
28    /// Multiple rigid bodies connected by constraints
29    Multibody,
30    /// Deformable solid with finite element discretization
31    DeformableSolid,
32    /// Mechanism with kinematic constraints
33    ConstrainedMechanism,
34}
35
36/// Configuration for mechanical system integration
37#[derive(Debug, Clone)]
38pub struct MechanicalConfig {
39    /// Type of mechanical system
40    pub system_type: MechanicalSystemType,
41    /// Time step size
42    pub dt: f64,
43    /// Integration method for position update
44    pub position_method: PositionIntegrationMethod,
45    /// Integration method for velocity update
46    pub velocity_method: VelocityIntegrationMethod,
47    /// Constraint enforcement method
48    pub constraint_method: ConstraintMethod,
49    /// Energy conservation tolerance
50    pub energy_tolerance: f64,
51    /// Maximum constraint violation tolerance
52    pub constraint_tolerance: f64,
53    /// Whether to use stabilization techniques
54    pub use_stabilization: bool,
55}
56
57impl Default for MechanicalConfig {
58    fn default() -> Self {
59        Self {
60            system_type: MechanicalSystemType::RigidBody,
61            dt: 0.01,
62            position_method: PositionIntegrationMethod::Verlet,
63            velocity_method: VelocityIntegrationMethod::Leapfrog,
64            constraint_method: ConstraintMethod::Lagrange,
65            energy_tolerance: 1e-6,
66            constraint_tolerance: 1e-8,
67            use_stabilization: true,
68        }
69    }
70}
71
72/// Position integration methods
73#[derive(Debug, Clone, Copy, PartialEq)]
74pub enum PositionIntegrationMethod {
75    /// Verlet integration (symplectic, energy conserving)
76    Verlet,
77    /// Velocity Verlet (explicit positions, implicit velocities)
78    VelocityVerlet,
79    /// Newmark-β method (popular in structural dynamics)
80    NewmarkBeta { beta: f64, gamma: f64 },
81    /// Central difference method
82    CentralDifference,
83}
84
85/// Velocity integration methods
86#[derive(Debug, Clone, Copy, PartialEq)]
87pub enum VelocityIntegrationMethod {
88    /// Leapfrog integration
89    Leapfrog,
90    /// Implicit Euler for damped systems
91    ImplicitEuler,
92    /// Crank-Nicolson for smooth dynamics
93    CrankNicolson,
94    /// Runge-Kutta 4th order
95    RungeKutta4,
96}
97
98/// Constraint enforcement methods
99#[derive(Debug, Clone, Copy, PartialEq)]
100pub enum ConstraintMethod {
101    /// Lagrange multipliers
102    Lagrange,
103    /// Penalty method
104    Penalty { stiffness: f64 },
105    /// Augmented Lagrangian
106    AugmentedLagrangian { penalty: f64 },
107    /// Stabilization (Baumgarte)
108    Baumgarte { alpha: f64, beta: f64 },
109}
110
111/// Rigid body state representation
112#[derive(Debug, Clone)]
113pub struct RigidBodyState {
114    /// Position (3D vector)
115    pub position: Array1<f64>,
116    /// Velocity (3D vector)
117    pub velocity: Array1<f64>,
118    /// Orientation (quaternion: w, x, y, z)
119    pub orientation: Array1<f64>,
120    /// Angular velocity (3D vector)
121    pub angular_velocity: Array1<f64>,
122}
123
124impl RigidBodyState {
125    /// Create a new rigid body state
126    pub fn new(
127        position: Array1<f64>,
128        velocity: Array1<f64>,
129        orientation: Array1<f64>,
130        angular_velocity: Array1<f64>,
131    ) -> Self {
132        Self {
133            position,
134            velocity,
135            orientation,
136            angular_velocity,
137        }
138    }
139
140    /// Get total kinetic energy
141    pub fn kinetic_energy(&self, mass: f64, inertia: &Array2<f64>) -> f64 {
142        let translational = 0.5 * mass * self.velocity.mapv(|v| v * v).sum();
143        let rotational = 0.5
144            * self
145                .angular_velocity
146                .dot(&inertia.dot(&self.angular_velocity));
147        translational + rotational
148    }
149
150    /// Normalize quaternion orientation
151    pub fn normalize_quaternion(&mut self) {
152        let norm = self.orientation.mapv(|q| q * q).sum().sqrt();
153        if norm > 1e-12 {
154            self.orientation /= norm;
155        }
156    }
157}
158
159/// Mechanical system properties
160pub struct MechanicalProperties {
161    /// Mass (scalar for single body, vector for multibody)
162    pub mass: Array1<f64>,
163    /// Inertia tensor(s)
164    pub inertia: Vec<Array2<f64>>,
165    /// Damping coefficients
166    pub damping: Array1<f64>,
167    /// External force function
168    pub external_forces: Option<ExternalForceFunction>,
169    /// Constraint equations
170    pub constraints: Vec<ConstraintFunction>,
171    /// Constraint Jacobians
172    pub constraint_jacobians: Vec<ConstraintJacobianFunction>,
173    /// Spring constants for harmonic oscillators (if applicable)
174    pub spring_constants: Vec<f64>,
175}
176
177impl Default for MechanicalProperties {
178    fn default() -> Self {
179        Self {
180            mass: Array1::ones(1),
181            inertia: vec![Array2::eye(3)],
182            damping: Array1::zeros(6),
183            external_forces: None,
184            constraints: Vec::new(),
185            constraint_jacobians: Vec::new(),
186            spring_constants: Vec::new(),
187        }
188    }
189}
190
191/// Result of mechanical integration step
192#[derive(Debug, Clone)]
193pub struct MechanicalIntegrationResult {
194    /// Updated state
195    pub state: RigidBodyState,
196    /// Constraint forces/torques
197    pub constraint_forces: Array1<f64>,
198    /// Energy drift
199    pub energy_drift: f64,
200    /// Constraint violation
201    pub constraint_violation: f64,
202    /// Integration statistics
203    pub stats: IntegrationStats,
204}
205
206/// Integration statistics for performance monitoring
207#[derive(Debug, Clone)]
208pub struct IntegrationStats {
209    /// Number of constraint iterations
210    pub constraint_iterations: usize,
211    /// Convergence achieved
212    pub converged: bool,
213    /// Time spent in force calculation
214    pub force_computation_time: f64,
215    /// Time spent in constraint enforcement
216    pub constraint_time: f64,
217}
218
219impl Default for IntegrationStats {
220    fn default() -> Self {
221        Self {
222            constraint_iterations: 0,
223            converged: true,
224            force_computation_time: 0.0,
225            constraint_time: 0.0,
226        }
227    }
228}
229
230/// Specialized integrator for mechanical systems
231pub struct MechanicalIntegrator {
232    config: MechanicalConfig,
233    properties: MechanicalProperties,
234    previous_state: Option<RigidBodyState>,
235    energy_history: Vec<f64>,
236}
237
238impl MechanicalIntegrator {
239    /// Create a new mechanical integrator
240    pub fn new(config: MechanicalConfig, properties: MechanicalProperties) -> Self {
241        Self {
242            config,
243            properties,
244            previous_state: None,
245            energy_history: Vec::new(),
246        }
247    }
248
249    /// Perform a single integration step
250    pub fn step(
251        &mut self,
252        t: f64,
253        state: &RigidBodyState,
254    ) -> IntegrateResult<MechanicalIntegrationResult> {
255        let start_time = std::time::Instant::now();
256
257        // Calculate forces and torques
258        let (forces, torques) = self.calculate_forces_torques(t, state)?;
259        let force_time = start_time.elapsed().as_secs_f64();
260
261        // Integrate based on selected method
262        let mut new_state = match self.config.position_method {
263            PositionIntegrationMethod::Verlet => self.verlet_step(state, &forces, &torques)?,
264            PositionIntegrationMethod::VelocityVerlet => {
265                self.velocity_verlet_step(state, &forces, &torques)?
266            }
267            PositionIntegrationMethod::NewmarkBeta { beta, gamma } => {
268                self.newmark_beta_step(state, &forces, &torques, beta, gamma)?
269            }
270            PositionIntegrationMethod::CentralDifference => {
271                self.central_difference_step(state, &forces, &torques)?
272            }
273        };
274
275        let constraint_start = std::time::Instant::now();
276
277        // Enforce constraints
278        let (constraint_forces, constraint_violation, iterations, converged) = self
279            .enforce_constraints(&mut new_state)
280            .map_err(|e| IntegrateError::ComputationError(e.to_string()))?;
281
282        let constraint_time = constraint_start.elapsed().as_secs_f64();
283
284        // Calculate energy drift
285        let current_energy = self.calculate_total_energy(&new_state);
286        let energy_drift = if let Some(ref prev_state) = self.previous_state {
287            let prev_energy = self.calculate_total_energy(prev_state);
288            (current_energy - prev_energy).abs() / prev_energy.max(1e-12)
289        } else {
290            0.0
291        };
292
293        self.energy_history.push(current_energy);
294        self.previous_state = Some(new_state.clone());
295
296        Ok(MechanicalIntegrationResult {
297            state: new_state,
298            constraint_forces,
299            energy_drift,
300            constraint_violation,
301            stats: IntegrationStats {
302                constraint_iterations: iterations,
303                converged,
304                force_computation_time: force_time,
305                constraint_time,
306            },
307        })
308    }
309
310    /// Calculate forces and torques acting on the system
311    fn calculate_forces_torques(
312        &self,
313        t: f64,
314        state: &RigidBodyState,
315    ) -> IntegrateResult<(Array1<f64>, Array1<f64>)> {
316        let n_bodies = self.properties.mass.len();
317        let mut forces = Array1::zeros(3 * n_bodies);
318        let mut torques = Array1::zeros(3 * n_bodies);
319
320        // External forces
321        if let Some(ref external_force_fn) = self.properties.external_forces {
322            let combined_state = Self::combine_position_velocity(state);
323            let external = external_force_fn(t, &combined_state, &state.velocity);
324
325            for i in 0..n_bodies {
326                forces
327                    .slice_mut(s![3 * i..3 * (i + 1)])
328                    .assign(&external.slice(s![3 * i..3 * (i + 1)]));
329            }
330        }
331
332        // Damping forces
333        for i in 0..n_bodies {
334            let damping_force = -&self.properties.damping.slice(s![3 * i..3 * (i + 1)])
335                * state.velocity.slice(s![3 * i..3 * (i + 1)]);
336            let current_force = forces.slice(s![3 * i..3 * (i + 1)]).to_owned();
337            forces
338                .slice_mut(s![3 * i..3 * (i + 1)])
339                .assign(&(&current_force + &damping_force));
340
341            // Angular damping
342            if self.properties.damping.len() > 3 * n_bodies {
343                let angular_damping = -&self
344                    .properties
345                    .damping
346                    .slice(s![3 * (n_bodies + i)..3 * (n_bodies + i + 1)])
347                    * state.angular_velocity.slice(s![3 * i..3 * (i + 1)]);
348                let current_torque = torques.slice(s![3 * i..3 * (i + 1)]).to_owned();
349                torques
350                    .slice_mut(s![3 * i..3 * (i + 1)])
351                    .assign(&(&current_torque + &angular_damping));
352            }
353        }
354
355        Ok((forces, torques))
356    }
357
358    /// Verlet integration step
359    fn verlet_step(
360        &self,
361        state: &RigidBodyState,
362        forces: &Array1<f64>,
363        torques: &Array1<f64>,
364    ) -> IntegrateResult<RigidBodyState> {
365        let dt = self.config.dt;
366        let dt2 = dt * dt;
367
368        // Position update: x_{n+1} = 2x_n - x_{n-1} + a_n * dt^2
369        let acceleration = self.calculate_acceleration(forces);
370        let angular_acceleration = self.calculate_angular_acceleration(torques, state);
371
372        let new_position = if let Some(ref prev_state) = self.previous_state {
373            2.0 * &state.position - &prev_state.position + &acceleration * dt2
374        } else {
375            // First step: use Euler's method
376            &state.position + &state.velocity * dt + 0.5 * &acceleration * dt2
377        };
378
379        // Velocity update: v_{n+1} = (x_{n+1} - x_{n-1}) / (2*dt)
380        let new_velocity = if let Some(ref prev_state) = self.previous_state {
381            (&new_position - &prev_state.position) / (2.0 * dt)
382        } else {
383            &state.velocity + &acceleration * dt
384        };
385
386        // Angular velocity update (similar to linear)
387        let new_angular_velocity = if let Some(ref prev_state) = self.previous_state {
388            &state.angular_velocity + &angular_acceleration * dt
389        } else {
390            &state.angular_velocity + &angular_acceleration * dt
391        };
392
393        // Orientation update using quaternion integration
394        let new_orientation =
395            self.integrate_quaternion(&state.orientation, &new_angular_velocity, dt);
396
397        Ok(RigidBodyState::new(
398            new_position,
399            new_velocity,
400            new_orientation,
401            new_angular_velocity,
402        ))
403    }
404
405    /// Velocity Verlet integration step
406    fn velocity_verlet_step(
407        &self,
408        state: &RigidBodyState,
409        forces: &Array1<f64>,
410        torques: &Array1<f64>,
411    ) -> IntegrateResult<RigidBodyState> {
412        let dt = self.config.dt;
413        let dt2 = dt * dt;
414
415        let acceleration = self.calculate_acceleration(forces);
416        let angular_acceleration = self.calculate_angular_acceleration(torques, state);
417
418        // Position update: x_{n+1} = x_n + v_n * dt + 0.5 * a_n * dt^2
419        let new_position = &state.position + &state.velocity * dt + 0.5 * &acceleration * dt2;
420
421        // Velocity update: v_{n+1} = v_n + 0.5 * (a_n + a_{n+1}) * dt
422        // For now, assume acceleration doesn't change much
423        let new_velocity = &state.velocity + &acceleration * dt;
424        let new_angular_velocity = &state.angular_velocity + &angular_acceleration * dt;
425
426        // Orientation update
427        let new_orientation =
428            self.integrate_quaternion(&state.orientation, &new_angular_velocity, dt);
429
430        Ok(RigidBodyState::new(
431            new_position,
432            new_velocity,
433            new_orientation,
434            new_angular_velocity,
435        ))
436    }
437
438    /// Newmark-β integration step
439    fn newmark_beta_step(
440        &self,
441        state: &RigidBodyState,
442        forces: &Array1<f64>,
443        torques: &Array1<f64>,
444        beta: f64,
445        gamma: f64,
446    ) -> IntegrateResult<RigidBodyState> {
447        let dt = self.config.dt;
448        let dt2 = dt * dt;
449
450        let acceleration = self.calculate_acceleration(forces);
451        let angular_acceleration = self.calculate_angular_acceleration(torques, state);
452
453        // Newmark-β formulas
454        let new_position =
455            &state.position + &state.velocity * dt + (0.5 - beta) * &acceleration * dt2;
456
457        let new_velocity = &state.velocity + (1.0 - gamma) * &acceleration * dt;
458
459        // Similar for angular quantities
460        let new_angular_velocity =
461            &state.angular_velocity + (1.0 - gamma) * &angular_acceleration * dt;
462        let new_orientation =
463            self.integrate_quaternion(&state.orientation, &new_angular_velocity, dt);
464
465        Ok(RigidBodyState::new(
466            new_position,
467            new_velocity,
468            new_orientation,
469            new_angular_velocity,
470        ))
471    }
472
473    /// Central difference integration step
474    fn central_difference_step(
475        &self,
476        state: &RigidBodyState,
477        forces: &Array1<f64>,
478        torques: &Array1<f64>,
479    ) -> IntegrateResult<RigidBodyState> {
480        // Similar to Verlet but with different formulation
481        self.verlet_step(state, forces, torques)
482    }
483
484    /// Calculate linear acceleration from forces
485    fn calculate_acceleration(&self, forces: &Array1<f64>) -> Array1<f64> {
486        let n_bodies = self.properties.mass.len();
487        let mut acceleration = Array1::zeros(forces.len());
488
489        for i in 0..n_bodies {
490            let mass = self.properties.mass[i];
491            if mass > 1e-12 {
492                let force_slice = forces.slice(s![3 * i..3 * (i + 1)]);
493                acceleration
494                    .slice_mut(s![3 * i..3 * (i + 1)])
495                    .assign(&force_slice.mapv(|f| f / mass));
496            }
497        }
498
499        acceleration
500    }
501
502    /// Calculate angular acceleration from torques
503    fn calculate_angular_acceleration(
504        &self,
505        torques: &Array1<f64>,
506        _state: &RigidBodyState,
507    ) -> Array1<f64> {
508        // For single rigid body, return 3D angular acceleration
509        if self.properties.inertia.len() == 1 && torques.len() >= 3 {
510            let inertia = &self.properties.inertia[0];
511            let mut angular_acceleration = Array1::zeros(3);
512
513            // Solve I * α = τ for α
514            // For simplicity, assume inertia is diagonal
515            for j in 0..3 {
516                let inertia_jj = inertia[[j, j]];
517                if inertia_jj > 1e-12 {
518                    angular_acceleration[j] = torques[j] / inertia_jj;
519                }
520            }
521
522            return angular_acceleration;
523        }
524
525        // For multibody systems
526        let n_bodies = self.properties.inertia.len();
527        let mut angular_acceleration = Array1::zeros(torques.len());
528
529        for i in 0..n_bodies {
530            if i < self.properties.inertia.len() {
531                let inertia = &self.properties.inertia[i];
532                let torque = torques.slice(s![3 * i..3 * (i + 1)]);
533
534                // Solve I * α = τ for α
535                // For simplicity, assume inertia is diagonal
536                for j in 0..3 {
537                    let inertia_jj = inertia[[j, j]];
538                    if inertia_jj > 1e-12 {
539                        angular_acceleration[3 * i + j] = torque[j] / inertia_jj;
540                    }
541                }
542            }
543        }
544
545        angular_acceleration
546    }
547
548    /// Integrate quaternion using angular velocity
549    fn integrate_quaternion(
550        &self,
551        quaternion: &Array1<f64>,
552        angular_velocity: &Array1<f64>,
553        dt: f64,
554    ) -> Array1<f64> {
555        // Quaternion integration: q_{n+1} = q_n + 0.5 * Ω(ω) * q_n * dt
556        // where Ω(ω) is the quaternion rate matrix
557
558        let omega_norm = angular_velocity.mapv(|w| w * w).sum().sqrt();
559
560        if omega_norm < 1e-12 {
561            return quaternion.clone();
562        }
563
564        let half_angle = 0.5 * omega_norm * dt;
565        let sin_half = half_angle.sin();
566        let cos_half = half_angle.cos();
567
568        let axis = angular_velocity / omega_norm;
569
570        // Rotation quaternion
571        let dq = Array1::from_vec(vec![
572            cos_half,
573            sin_half * axis[0],
574            sin_half * axis[1],
575            sin_half * axis[2],
576        ]);
577
578        // Quaternion multiplication: q_new = dq * q_old
579        Self::quaternion_multiply(&dq, quaternion)
580    }
581
582    /// Multiply two quaternions
583    fn quaternion_multiply(q1: &Array1<f64>, q2: &Array1<f64>) -> Array1<f64> {
584        let w1 = q1[0];
585        let x1 = q1[1];
586        let y1 = q1[2];
587        let z1 = q1[3];
588        let w2 = q2[0];
589        let x2 = q2[1];
590        let y2 = q2[2];
591        let z2 = q2[3];
592
593        Array1::from_vec(vec![
594            w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
595            w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
596            w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
597            w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
598        ])
599    }
600
601    /// Enforce constraints using selected method
602    fn enforce_constraints(&mut self, state: &mut RigidBodyState) -> IntegrationResult {
603        if self.properties.constraints.is_empty() {
604            return Ok((Array1::zeros(0), 0.0, 0, true));
605        }
606
607        match self.config.constraint_method {
608            ConstraintMethod::Lagrange => self.enforce_lagrange_constraints(state),
609            ConstraintMethod::Penalty { stiffness } => {
610                self.enforce_penalty_constraints(state, stiffness)
611            }
612            ConstraintMethod::AugmentedLagrangian { penalty } => {
613                self.enforce_augmented_lagrangian_constraints(state, penalty)
614            }
615            ConstraintMethod::Baumgarte { alpha, beta } => {
616                self.enforce_baumgarte_stabilization(state, alpha, beta)
617            }
618        }
619    }
620
621    /// Lagrange multiplier constraint enforcement
622    fn enforce_lagrange_constraints(&mut self, state: &mut RigidBodyState) -> IntegrationResult {
623        let max_iterations = 10;
624        let tolerance = self.config.constraint_tolerance;
625
626        let mut lambda = Array1::zeros(self.properties.constraints.len());
627        let mut converged = false;
628        let mut iterations_used = 0;
629
630        for iteration in 0..max_iterations {
631            iterations_used = iteration + 1;
632
633            // Evaluate constraints
634            let combined_pos = Self::combine_position_velocity(state);
635            let mut constraint_values = Array1::zeros(self.properties.constraints.len());
636
637            for (i, constraint) in self.properties.constraints.iter().enumerate() {
638                let c_val = constraint(&combined_pos);
639                constraint_values[i] = c_val[0]; // Assume scalar constraints for simplicity
640            }
641
642            let violation = constraint_values.mapv(|c| c * c).sum().sqrt();
643
644            if violation < tolerance {
645                converged = true;
646                break;
647            }
648
649            // Check for NaN or excessive constraint violations and abort if detected
650            if violation.is_nan() || !violation.is_finite() || violation > 1e6 {
651                // Reset to a more stable configuration
652                state
653                    .position
654                    .mapv_inplace(|x| if x.is_nan() || !x.is_finite() { 0.0 } else { x });
655                state
656                    .velocity
657                    .mapv_inplace(|x| if x.is_nan() || !x.is_finite() { 0.0 } else { x });
658                break;
659            }
660
661            // Proper Newton-Raphson iteration with constraint Jacobian
662            if self.properties.constraint_jacobians.len() == self.properties.constraints.len() {
663                // Calculate constraint Jacobian matrix
664                let num_coords = combined_pos.len();
665                let num_constraints = constraint_values.len();
666                let mut jacobian = Array2::zeros((num_constraints, num_coords));
667
668                for (i, jacobian_func) in self.properties.constraint_jacobians.iter().enumerate() {
669                    let jac_row = jacobian_func(&combined_pos);
670                    for j in 0..num_coords.min(jac_row.ncols()) {
671                        jacobian[(i, j)] = jac_row[(0, j)];
672                    }
673                }
674
675                // Solve for Lagrange multipliers: J * J^T * λ = -C
676                let jjt = jacobian.dot(&jacobian.t());
677                if let Ok(delta_lambda) = self.solve_constraint_system(&jjt, &(-&constraint_values))
678                {
679                    lambda += &delta_lambda;
680
681                    // Apply position correction: Δq = -J^T * λ
682                    let position_correction = jacobian.t().dot(&delta_lambda);
683
684                    // Apply correction with damping to ensure stability
685                    let damping = 0.5; // Even more conservative damping
686                    for i in 0..state.position.len().min(position_correction.len()) {
687                        if position_correction[i].is_finite() && position_correction[i].abs() < 1.0
688                        {
689                            state.position[i] -= damping * position_correction[i];
690                        }
691                    }
692
693                    // Additional constraint enforcement - project back to constraint manifold
694                    if violation > 1e3 {
695                        // For very large violations, use a more drastic correction
696                        self.project_to_constraint_manifold(state)?;
697                    }
698                } else {
699                    // Fallback: simple proportional correction with proper scaling
700                    let correction_factor = 0.01; // Much smaller factor
701                    if num_constraints > 0 && combined_pos.len() >= 4 {
702                        // For double pendulum: distribute correction appropriately
703                        let correction_per_coord =
704                            constraint_values.sum() / combined_pos.len() as f64;
705                        for i in 0..state.position.len() {
706                            state.position[i] -= correction_factor * correction_per_coord;
707                        }
708                    }
709                }
710            } else {
711                // Fallback method when Jacobians are not available
712                let correction_factor = 0.01;
713                if !constraint_values.is_empty() && state.position.len() >= 4 {
714                    // Simple correction distributed across coordinates
715                    let avg_violation = constraint_values.iter().map(|c| c.abs()).sum::<f64>()
716                        / constraint_values.len() as f64;
717                    for i in 0..state.position.len() {
718                        // Small correction to prevent instability
719                        state.position[i] -= correction_factor * avg_violation * (i as f64 + 1.0)
720                            / state.position.len() as f64;
721                    }
722                }
723            }
724        }
725
726        let final_violation = {
727            let combined_pos = Self::combine_position_velocity(state);
728            let mut total = 0.0;
729            for constraint in &self.properties.constraints {
730                let c_val = constraint(&combined_pos);
731                let violation = c_val[0];
732                // Protect against numerical issues
733                if violation.is_finite() {
734                    total += violation * violation;
735                } else {
736                    total = f64::INFINITY;
737                    break;
738                }
739            }
740            if total.is_finite() {
741                total.sqrt()
742            } else {
743                f64::INFINITY
744            }
745        };
746
747        Ok((lambda, final_violation, iterations_used, converged))
748    }
749
750    /// Penalty method constraint enforcement
751    fn enforce_penalty_constraints(
752        &self,
753        state: &mut RigidBodyState,
754        stiffness: f64,
755    ) -> IntegrationResult {
756        let combined_pos = Self::combine_position_velocity(state);
757        let mut constraint_forces = Array1::zeros(state.position.len());
758        let mut total_violation = 0.0;
759
760        for constraint in &self.properties.constraints {
761            let c_val = constraint(&combined_pos);
762            let violation = c_val[0];
763            total_violation += violation * violation;
764
765            // Apply penalty force: F = -k * C(q)
766            let penalty_force = -stiffness * violation;
767
768            // Distribute force (simplified)
769            constraint_forces[0] += penalty_force;
770        }
771
772        // Apply penalty forces to state
773        let dt = self.config.dt;
774        let mass = self.properties.mass[0];
775        let acceleration = constraint_forces.mapv(|f| f / mass);
776        state.velocity = &state.velocity + &acceleration * dt;
777
778        Ok((constraint_forces, total_violation.sqrt(), 1, true))
779    }
780
781    /// Augmented Lagrangian constraint enforcement
782    fn enforce_augmented_lagrangian_constraints(
783        &mut self,
784        state: &mut RigidBodyState,
785        penalty: f64,
786    ) -> IntegrationResult {
787        // Combine Lagrange multipliers with penalty method
788        let (lambda, violation, iter1, conv1) = self.enforce_lagrange_constraints(state)?;
789        let (penalty_forces, violation2, iter2, conv2) =
790            self.enforce_penalty_constraints(state, penalty)?;
791
792        Ok((
793            lambda + penalty_forces,
794            violation,
795            iter1 + iter2,
796            conv1 && conv2,
797        ))
798    }
799
800    /// Baumgarte stabilization
801    fn enforce_baumgarte_stabilization(
802        &self,
803        state: &mut RigidBodyState,
804        alpha: f64,
805        beta: f64,
806    ) -> IntegrationResult {
807        // Baumgarte stabilization: C̈ + 2α*Ċ + β²*C = 0
808        // This requires constraint velocity and acceleration
809        // For simplicity, implement basic position correction
810
811        let combined_pos = Self::combine_position_velocity(state);
812        let mut total_violation = 0.0;
813
814        for constraint in &self.properties.constraints {
815            let c_val = constraint(&combined_pos);
816            let violation = c_val[0];
817            total_violation += violation * violation;
818
819            // Apply Baumgarte correction
820            let correction = -beta * beta * violation;
821            state.position[0] += correction * self.config.dt * self.config.dt;
822        }
823
824        Ok((Array1::zeros(0), total_violation.sqrt(), 1, true))
825    }
826
827    /// Solve constraint system with numerical stability checks
828    fn solve_constraint_system(
829        &self,
830        matrix: &Array2<f64>,
831        rhs: &Array1<f64>,
832    ) -> IntegrateResult<Array1<f64>> {
833        let n = matrix.nrows();
834        if n == 0 || n != matrix.ncols() || n != rhs.len() {
835            return Err(IntegrateError::ValueError(
836                "Invalid matrix dimensions".to_string(),
837            ));
838        }
839
840        // Check for near-singular matrix
841        let det_check = matrix.mapv(|x| x.abs()).sum() / (n * n) as f64;
842        if det_check < 1e-14 {
843            return Err(IntegrateError::ValueError(
844                "Nearly singular constraint matrix".to_string(),
845            ));
846        }
847
848        // Simple Gauss elimination with partial pivoting
849        let mut aug_matrix = Array2::zeros((n, n + 1));
850
851        // Set up augmented matrix
852        for i in 0..n {
853            for j in 0..n {
854                aug_matrix[(i, j)] = matrix[(i, j)];
855            }
856            aug_matrix[(i, n)] = rhs[i];
857        }
858
859        // Forward elimination with partial pivoting
860        for i in 0..n {
861            // Find pivot
862            let mut max_row = i;
863            for k in (i + 1)..n {
864                if aug_matrix[(k, i)].abs() > aug_matrix[(max_row, i)].abs() {
865                    max_row = k;
866                }
867            }
868
869            // Check for near-zero pivot
870            if aug_matrix[(max_row, i)].abs() < 1e-12 {
871                return Err(IntegrateError::ValueError(
872                    "Singular matrix in constraint solving".to_string(),
873                ));
874            }
875
876            // Swap rows if needed
877            if max_row != i {
878                for j in 0..=n {
879                    let temp = aug_matrix[(i, j)];
880                    aug_matrix[(i, j)] = aug_matrix[(max_row, j)];
881                    aug_matrix[(max_row, j)] = temp;
882                }
883            }
884
885            // Eliminate
886            for k in (i + 1)..n {
887                let factor = aug_matrix[(k, i)] / aug_matrix[(i, i)];
888                for j in i..=n {
889                    aug_matrix[(k, j)] -= factor * aug_matrix[(i, j)];
890                }
891            }
892        }
893
894        // Back substitution
895        let mut solution = Array1::zeros(n);
896        for i in (0..n).rev() {
897            solution[i] = aug_matrix[(i, n)];
898            for j in (i + 1)..n {
899                solution[i] -= aug_matrix[(i, j)] * solution[j];
900            }
901            solution[i] /= aug_matrix[(i, i)];
902
903            // Check for NaN or infinite solutions
904            if !solution[i].is_finite() {
905                return Err(IntegrateError::ValueError(
906                    "Non-finite solution in constraint solving".to_string(),
907                ));
908            }
909        }
910
911        Ok(solution)
912    }
913
914    /// Project system back to constraint manifold for severe violations
915    fn project_to_constraint_manifold(&self, state: &mut RigidBodyState) -> IntegrateResult<()> {
916        // For double pendulum, directly project to valid configuration
917        if self.properties.constraints.len() == 2 && state.position.len() >= 6 {
918            // Get parameters from the constraints (assuming they're for double pendulum)
919            let l1 = 1.0; // Typical values - could be extracted from constraints
920            let l2 = 0.8;
921
922            // Get current positions
923            let x1 = state.position[0];
924            let y1 = state.position[1];
925            let x2 = state.position[3];
926            let y2 = state.position[4];
927
928            // Project first pendulum to correct length
929            let r1 = (x1 * x1 + y1 * y1).sqrt();
930            if r1 > 1e-6 {
931                state.position[0] = x1 * l1 / r1;
932                state.position[1] = y1 * l1 / r1;
933            } else {
934                // If at origin, place at reference position
935                state.position[0] = 0.0;
936                state.position[1] = -l1;
937            }
938
939            // Project second pendulum to correct length relative to first
940            let dx = x2 - state.position[0];
941            let dy = y2 - state.position[1];
942            let r2 = (dx * dx + dy * dy).sqrt();
943            if r2 > 1e-6 {
944                state.position[3] = state.position[0] + dx * l2 / r2;
945                state.position[4] = state.position[1] + dy * l2 / r2;
946            } else {
947                // If coincident, place at reference position
948                state.position[3] = state.position[0];
949                state.position[4] = state.position[1] - l2;
950            }
951
952            // Adjust velocities to be consistent with constraints (zero radial component)
953            let x1_new = state.position[0];
954            let y1_new = state.position[1];
955            let x2_new = state.position[3];
956            let y2_new = state.position[4];
957
958            // For first pendulum: v · r = 0 (velocity perpendicular to radius)
959            let vx1 = state.velocity[0];
960            let vy1 = state.velocity[1];
961            let radial_component1 = (vx1 * x1_new + vy1 * y1_new) / (l1 * l1);
962            state.velocity[0] = vx1 - radial_component1 * x1_new;
963            state.velocity[1] = vy1 - radial_component1 * y1_new;
964
965            // For second pendulum: (v2 - v1) · (r2 - r1) = 0
966            let vx2 = state.velocity[3];
967            let vy2 = state.velocity[4];
968            let rel_vx = vx2 - state.velocity[0];
969            let rel_vy = vy2 - state.velocity[1];
970            let rel_dx = x2_new - x1_new;
971            let rel_dy = y2_new - y1_new;
972            let radial_component2 = (rel_vx * rel_dx + rel_vy * rel_dy) / (l2 * l2);
973            state.velocity[3] = state.velocity[0] + rel_vx - radial_component2 * rel_dx;
974            state.velocity[4] = state.velocity[1] + rel_vy - radial_component2 * rel_dy;
975        }
976
977        Ok(())
978    }
979
980    /// Calculate total energy of the system
981    fn calculate_total_energy(&self, state: &RigidBodyState) -> f64 {
982        let n_bodies = self.properties.mass.len();
983        let mut total_kinetic = 0.0;
984
985        // Calculate kinetic energy for each body
986        for i in 0..n_bodies {
987            let mass = self.properties.mass[i];
988            let inertia = &self.properties.inertia[i];
989
990            // Translational kinetic energy
991            let vel_start = 3 * i;
992            let vel_end = 3 * (i + 1);
993            if vel_end <= state.velocity.len() {
994                let body_velocity = state.velocity.slice(s![vel_start..vel_end]);
995                let translational = 0.5 * mass * body_velocity.mapv(|v| v * v).sum();
996
997                // Rotational kinetic energy
998                let mut rotational = 0.0;
999                if vel_end <= state.angular_velocity.len() {
1000                    let body_angular_vel = state.angular_velocity.slice(s![vel_start..vel_end]);
1001                    rotational = 0.5 * body_angular_vel.dot(&inertia.dot(&body_angular_vel));
1002                }
1003
1004                total_kinetic += translational + rotational;
1005            }
1006        }
1007
1008        // Add potential energy based on system type
1009        let potential_energy = if self.properties.external_forces.is_some() {
1010            if self.properties.mass.len() == 1 && !self.properties.spring_constants.is_empty() {
1011                // For single-body harmonic oscillator: PE = 0.5 * k * x^2
1012                let k = self.properties.spring_constants[0];
1013                0.5 * k * state.position[0] * state.position[0]
1014            } else if self.properties.mass.len() == 2 && self.properties.constraints.len() == 2 {
1015                // For double pendulum: gravitational potential energy
1016                let g = 9.81;
1017                let m1 = self.properties.mass[0];
1018                let m2 = self.properties.mass[1];
1019                let y1 = state.position[1]; // y-coordinate of first mass
1020                let y2 = state.position[4]; // y-coordinate of second mass
1021                m1 * g * y1 + m2 * g * y2
1022            } else {
1023                0.0
1024            }
1025        } else {
1026            0.0
1027        };
1028
1029        total_kinetic + potential_energy
1030    }
1031
1032    /// Combine position and velocity into a single state vector
1033    fn combine_position_velocity(state: &RigidBodyState) -> Array1<f64> {
1034        let mut combined = Array1::zeros(state.position.len() + state.velocity.len());
1035        combined
1036            .slice_mut(s![..state.position.len()])
1037            .assign(&state.position);
1038        combined
1039            .slice_mut(s![state.position.len()..])
1040            .assign(&state.velocity);
1041        combined
1042    }
1043
1044    /// Get energy conservation statistics
1045    pub fn energy_statistics(&self) -> (f64, f64, f64) {
1046        if self.energy_history.len() < 2 {
1047            return (0.0, 0.0, 0.0);
1048        }
1049
1050        let initial_energy = self.energy_history[0];
1051        let current_energy = *self.energy_history.last().unwrap();
1052        let relative_drift = (current_energy - initial_energy).abs() / initial_energy.max(1e-12);
1053
1054        let max_energy = self
1055            .energy_history
1056            .iter()
1057            .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1058        let min_energy = self
1059            .energy_history
1060            .iter()
1061            .fold(f64::INFINITY, |a, &b| a.min(b));
1062        let max_drift = (max_energy - min_energy) / initial_energy.max(1e-12);
1063
1064        (relative_drift, max_drift, current_energy)
1065    }
1066}
1067
1068/// Factory functions for common mechanical systems
1069pub mod systems {
1070    use super::*;
1071
1072    /// Create a simple rigid body system
1073    pub fn rigid_body(
1074        mass: f64,
1075        inertia: Array2<f64>,
1076        initial_position: Array1<f64>,
1077        initial_velocity: Array1<f64>,
1078        initial_orientation: Array1<f64>,
1079        initial_angular_velocity: Array1<f64>,
1080    ) -> (MechanicalConfig, MechanicalProperties, RigidBodyState) {
1081        let config = MechanicalConfig {
1082            system_type: MechanicalSystemType::RigidBody,
1083            ..Default::default()
1084        };
1085
1086        let properties = MechanicalProperties {
1087            mass: Array1::from_vec(vec![mass]),
1088            inertia: vec![inertia],
1089            ..Default::default()
1090        };
1091
1092        let state = RigidBodyState::new(
1093            initial_position,
1094            initial_velocity,
1095            initial_orientation,
1096            initial_angular_velocity,
1097        );
1098
1099        (config, properties, state)
1100    }
1101
1102    /// Create a damped oscillator system
1103    pub fn damped_oscillator(
1104        mass: f64,
1105        stiffness: f64,
1106        damping: f64,
1107        initial_position: f64,
1108        initial_velocity: f64,
1109    ) -> (MechanicalConfig, MechanicalProperties, RigidBodyState) {
1110        let config = MechanicalConfig::default();
1111
1112        let spring_force = Box::new(move |t: f64, pos: &Array1<f64>, vel: &Array1<f64>| {
1113            Array1::from_vec(vec![-stiffness * pos[0] - damping * vel[0], 0.0, 0.0])
1114        });
1115
1116        let properties = MechanicalProperties {
1117            mass: Array1::from_vec(vec![mass]),
1118            inertia: vec![Array2::eye(3)],
1119            damping: Array1::from_vec(vec![damping, 0.0, 0.0]),
1120            external_forces: Some(spring_force),
1121            spring_constants: vec![stiffness],
1122            ..Default::default()
1123        };
1124
1125        let state = RigidBodyState::new(
1126            Array1::from_vec(vec![initial_position, 0.0, 0.0]),
1127            Array1::from_vec(vec![initial_velocity, 0.0, 0.0]),
1128            Array1::from_vec(vec![1.0, 0.0, 0.0, 0.0]), // Identity quaternion
1129            Array1::zeros(3),
1130        );
1131
1132        (config, properties, state)
1133    }
1134
1135    /// Create a double pendulum system (multibody with constraints)
1136    pub fn double_pendulum(
1137        m1: f64,
1138        m2: f64,
1139        l1: f64,
1140        l2: f64,
1141        initial_angles: [f64; 2],
1142        initial_velocities: [f64; 2],
1143    ) -> (MechanicalConfig, MechanicalProperties, RigidBodyState) {
1144        let config = MechanicalConfig {
1145            system_type: MechanicalSystemType::Multibody,
1146            constraint_method: ConstraintMethod::Lagrange,
1147            ..Default::default()
1148        };
1149
1150        // Gravitational forces
1151        let gravity_force = Box::new(move |_t: f64, _pos: &Array1<f64>, vel: &Array1<f64>| {
1152            Array1::from_vec(vec![0.0, -m1 * 9.81, 0.0, 0.0, -m2 * 9.81, 0.0])
1153        });
1154
1155        // Constraint equations for pendulum joints
1156        let constraint1 = Box::new(move |pos: &Array1<f64>| {
1157            // First pendulum constraint: |r1| = l1
1158            let x1 = pos[0];
1159            let y1 = pos[1];
1160            Array1::from_vec(vec![x1 * x1 + y1 * y1 - l1 * l1])
1161        });
1162
1163        let constraint2 = Box::new(move |pos: &Array1<f64>| {
1164            // Second pendulum constraint: |r2 - r1| = l2
1165            let x1 = pos[0];
1166            let y1 = pos[1];
1167            let x2 = pos[3];
1168            let y2 = pos[4];
1169            let dx = x2 - x1;
1170            let dy = y2 - y1;
1171            Array1::from_vec(vec![dx * dx + dy * dy - l2 * l2])
1172        });
1173
1174        // Constraint Jacobians for better numerical stability
1175        let jacobian1 = Box::new(move |pos: &Array1<f64>| {
1176            // Jacobian of constraint 1: C1 = x1² + y1² - l1²
1177            let x1 = pos[0];
1178            let y1 = pos[1];
1179            let mut jac = Array2::zeros((1, pos.len()));
1180            jac[(0, 0)] = 2.0 * x1; // ∂C1/∂x1
1181            jac[(0, 1)] = 2.0 * y1; // ∂C1/∂y1
1182                                    // All other entries are zero
1183            jac
1184        });
1185
1186        let jacobian2 = Box::new(move |pos: &Array1<f64>| {
1187            // Jacobian of constraint 2: C2 = (x2-x1)² + (y2-y1)² - l2²
1188            let x1 = pos[0];
1189            let y1 = pos[1];
1190            let x2 = pos[3];
1191            let y2 = pos[4];
1192            let dx = x2 - x1;
1193            let dy = y2 - y1;
1194            let mut jac = Array2::zeros((1, pos.len()));
1195            jac[(0, 0)] = -2.0 * dx; // ∂C2/∂x1
1196            jac[(0, 1)] = -2.0 * dy; // ∂C2/∂y1
1197                                     // pos[2] is z1, not used
1198            jac[(0, 3)] = 2.0 * dx; // ∂C2/∂x2
1199            jac[(0, 4)] = 2.0 * dy; // ∂C2/∂y2
1200                                    // pos[5] is z2, not used
1201            jac
1202        });
1203
1204        let properties = MechanicalProperties {
1205            mass: Array1::from_vec(vec![m1, m2]),
1206            inertia: vec![Array2::eye(3), Array2::eye(3)],
1207            external_forces: Some(gravity_force),
1208            constraints: vec![constraint1, constraint2],
1209            constraint_jacobians: vec![jacobian1, jacobian2],
1210            ..Default::default()
1211        };
1212
1213        // Convert initial _angles to Cartesian coordinates
1214        let x1 = l1 * initial_angles[0].sin();
1215        let y1 = -l1 * initial_angles[0].cos();
1216        let x2 = x1 + l2 * initial_angles[1].sin();
1217        let y2 = y1 - l2 * initial_angles[1].cos();
1218
1219        let vx1 = l1 * initial_velocities[0] * initial_angles[0].cos();
1220        let vy1 = l1 * initial_velocities[0] * initial_angles[0].sin();
1221        let vx2 = vx1 + l2 * initial_velocities[1] * initial_angles[1].cos();
1222        let vy2 = vy1 + l2 * initial_velocities[1] * initial_angles[1].sin();
1223
1224        let state = RigidBodyState::new(
1225            Array1::from_vec(vec![x1, y1, 0.0, x2, y2, 0.0]),
1226            Array1::from_vec(vec![vx1, vy1, 0.0, vx2, vy2, 0.0]),
1227            Array1::from_vec(vec![1.0, 0.0, 0.0, 0.0]), // Identity quaternion
1228            Array1::zeros(6), // Angular velocity for 2 bodies (3 components each)
1229        );
1230
1231        (config, properties, state)
1232    }
1233}
1234
1235#[cfg(test)]
1236mod tests {
1237    use crate::ode::{
1238        mechanical::systems, MechanicalConfig, MechanicalIntegrator, MechanicalProperties,
1239        MechanicalSystemType, RigidBodyState,
1240    };
1241    use approx::assert_abs_diff_eq;
1242    use scirs2_core::ndarray::{Array1, Array2};
1243
1244    #[test]
1245    fn test_rigid_body_state_creation() {
1246        let position = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1247        let velocity = Array1::from_vec(vec![0.1, 0.2, 0.3]);
1248        let orientation = Array1::from_vec(vec![1.0, 0.0, 0.0, 0.0]);
1249        let angular_velocity = Array1::from_vec(vec![0.01, 0.02, 0.03]);
1250
1251        let state = RigidBodyState::new(
1252            position.clone(),
1253            velocity.clone(),
1254            orientation.clone(),
1255            angular_velocity.clone(),
1256        );
1257
1258        assert_eq!(state.position, position);
1259        assert_eq!(state.velocity, velocity);
1260        assert_eq!(state.orientation, orientation);
1261        assert_eq!(state.angular_velocity, angular_velocity);
1262    }
1263
1264    #[test]
1265    fn test_kinetic_energy_calculation() {
1266        let mass = 2.0;
1267        let inertia = Array2::eye(3) * 0.1;
1268
1269        let state = RigidBodyState::new(
1270            Array1::zeros(3),
1271            Array1::from_vec(vec![1.0, 0.0, 0.0]),
1272            Array1::from_vec(vec![1.0, 0.0, 0.0, 0.0]),
1273            Array1::from_vec(vec![0.0, 0.0, 1.0]),
1274        );
1275
1276        let ke = state.kinetic_energy(mass, &inertia);
1277        let expected = 0.5 * mass * 1.0 + 0.5 * 0.1 * 1.0; // Translational + rotational
1278        assert_abs_diff_eq!(ke, expected, epsilon = 1e-10);
1279    }
1280
1281    #[test]
1282    fn test_quaternion_normalization() {
1283        let mut state = RigidBodyState::new(
1284            Array1::zeros(3),
1285            Array1::zeros(3),
1286            Array1::from_vec(vec![2.0, 1.0, 1.0, 1.0]), // Non-normalized
1287            Array1::zeros(3),
1288        );
1289
1290        state.normalize_quaternion();
1291
1292        let norm = state.orientation.mapv(|q| q * q).sum().sqrt();
1293        assert_abs_diff_eq!(norm, 1.0, epsilon = 1e-10);
1294    }
1295
1296    #[test]
1297    fn test_mechanical_integrator_creation() {
1298        let config = MechanicalConfig::default();
1299        let properties = MechanicalProperties::default();
1300
1301        let integrator = MechanicalIntegrator::new(config, properties);
1302
1303        assert_eq!(
1304            integrator.config.system_type,
1305            MechanicalSystemType::RigidBody
1306        );
1307        assert!(integrator.previous_state.is_none());
1308        assert!(integrator.energy_history.is_empty());
1309    }
1310
1311    #[test]
1312    fn test_damped_oscillator_system() {
1313        let (config, properties, state) = systems::damped_oscillator(
1314            1.0,  // mass
1315            10.0, // stiffness
1316            0.1,  // damping
1317            1.0,  // initial position
1318            0.0,  // initial velocity
1319        );
1320
1321        assert_eq!(config.system_type, MechanicalSystemType::RigidBody);
1322        assert_eq!(properties.mass[0], 1.0);
1323        assert_abs_diff_eq!(state.position[0], 1.0, epsilon = 1e-10);
1324        assert!(properties.external_forces.is_some());
1325    }
1326
1327    #[test]
1328    fn test_double_pendulum_system() {
1329        let (config, properties, state) = systems::double_pendulum(
1330            1.0,        // m1
1331            0.5,        // m2
1332            1.0,        // l1
1333            0.8,        // l2
1334            [0.1, 0.2], // initial angles
1335            [0.0, 0.0], // initial velocities
1336        );
1337
1338        assert_eq!(config.system_type, MechanicalSystemType::Multibody);
1339        assert_eq!(properties.mass.len(), 2);
1340        assert_eq!(properties.constraints.len(), 2);
1341        assert!(properties.external_forces.is_some());
1342
1343        // Check that positions satisfy initial angle constraints
1344        let x1 = state.position[0];
1345        let y1 = state.position[1];
1346        let r1 = (x1 * x1 + y1 * y1).sqrt();
1347        assert_abs_diff_eq!(r1, 1.0, epsilon = 1e-10); // Should be l1
1348    }
1349
1350    #[test]
1351    fn test_simple_integration_step() {
1352        let (config, properties, state) = systems::rigid_body(
1353            1.0,                                        // mass
1354            Array2::eye(3),                             // inertia
1355            Array1::zeros(3),                           // position
1356            Array1::from_vec(vec![1.0, 0.0, 0.0]),      // velocity
1357            Array1::from_vec(vec![1.0, 0.0, 0.0, 0.0]), // orientation
1358            Array1::zeros(3),                           // angular velocity
1359        );
1360
1361        let mut integrator = MechanicalIntegrator::new(config, properties);
1362
1363        let result = integrator.step(0.0, &state).unwrap();
1364
1365        // After one time step, position should have changed
1366        assert!(result.state.position[0] > 0.0);
1367        assert!(result.stats.converged);
1368    }
1369
1370    #[test]
1371    fn test_energy_conservation() {
1372        let (mut config, properties, state) = systems::damped_oscillator(
1373            1.0,  // mass
1374            10.0, // stiffness
1375            0.0,  // no damping for energy conservation test
1376            1.0,  // initial position
1377            0.0,  // initial velocity
1378        );
1379
1380        config.dt = 0.001; // Small time step for better conservation
1381        let mut integrator = MechanicalIntegrator::new(config, properties);
1382
1383        let _initial_energy = integrator.calculate_total_energy(&state);
1384        let mut current_state = state;
1385
1386        // Integrate for several steps
1387        for i in 0..100 {
1388            let result = integrator.step(i as f64 * 0.001, &current_state).unwrap();
1389            current_state = result.state;
1390        }
1391
1392        let (relative_drift, max_drift, current_energy) = integrator.energy_statistics();
1393
1394        // Energy should be reasonably conserved (within a few percent)
1395        assert!(
1396            relative_drift < 0.1,
1397            "Energy drift too large: {relative_drift}"
1398        );
1399    }
1400}