1use crate::error::{IntegrateError, IntegrateResult};
8use scirs2_core::ndarray::{s, Array1, Array2};
9
10type ExternalForceFunction =
12 Box<dyn Fn(f64, &Array1<f64>, &Array1<f64>) -> Array1<f64> + Send + Sync>;
13
14type ConstraintFunction = Box<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
16
17type ConstraintJacobianFunction = Box<dyn Fn(&Array1<f64>) -> Array2<f64> + Send + Sync>;
19
20type IntegrationResult = Result<(Array1<f64>, f64, usize, bool), Box<dyn std::error::Error>>;
22
23#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum MechanicalSystemType {
26 RigidBody,
28 Multibody,
30 DeformableSolid,
32 ConstrainedMechanism,
34}
35
36#[derive(Debug, Clone)]
38pub struct MechanicalConfig {
39 pub system_type: MechanicalSystemType,
41 pub dt: f64,
43 pub position_method: PositionIntegrationMethod,
45 pub velocity_method: VelocityIntegrationMethod,
47 pub constraint_method: ConstraintMethod,
49 pub energy_tolerance: f64,
51 pub constraint_tolerance: f64,
53 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#[derive(Debug, Clone, Copy, PartialEq)]
74pub enum PositionIntegrationMethod {
75 Verlet,
77 VelocityVerlet,
79 NewmarkBeta { beta: f64, gamma: f64 },
81 CentralDifference,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq)]
87pub enum VelocityIntegrationMethod {
88 Leapfrog,
90 ImplicitEuler,
92 CrankNicolson,
94 RungeKutta4,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
100pub enum ConstraintMethod {
101 Lagrange,
103 Penalty { stiffness: f64 },
105 AugmentedLagrangian { penalty: f64 },
107 Baumgarte { alpha: f64, beta: f64 },
109}
110
111#[derive(Debug, Clone)]
113pub struct RigidBodyState {
114 pub position: Array1<f64>,
116 pub velocity: Array1<f64>,
118 pub orientation: Array1<f64>,
120 pub angular_velocity: Array1<f64>,
122}
123
124impl RigidBodyState {
125 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 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 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
159pub struct MechanicalProperties {
161 pub mass: Array1<f64>,
163 pub inertia: Vec<Array2<f64>>,
165 pub damping: Array1<f64>,
167 pub external_forces: Option<ExternalForceFunction>,
169 pub constraints: Vec<ConstraintFunction>,
171 pub constraint_jacobians: Vec<ConstraintJacobianFunction>,
173 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#[derive(Debug, Clone)]
193pub struct MechanicalIntegrationResult {
194 pub state: RigidBodyState,
196 pub constraint_forces: Array1<f64>,
198 pub energy_drift: f64,
200 pub constraint_violation: f64,
202 pub stats: IntegrationStats,
204}
205
206#[derive(Debug, Clone)]
208pub struct IntegrationStats {
209 pub constraint_iterations: usize,
211 pub converged: bool,
213 pub force_computation_time: f64,
215 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
230pub struct MechanicalIntegrator {
232 config: MechanicalConfig,
233 properties: MechanicalProperties,
234 previous_state: Option<RigidBodyState>,
235 energy_history: Vec<f64>,
236}
237
238impl MechanicalIntegrator {
239 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 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 let (forces, torques) = self.calculate_forces_torques(t, state)?;
259 let force_time = start_time.elapsed().as_secs_f64();
260
261 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 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 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 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 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 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(&(¤t_force + &damping_force));
340
341 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(&(¤t_torque + &angular_damping));
352 }
353 }
354
355 Ok((forces, torques))
356 }
357
358 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 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 &state.position + &state.velocity * dt + 0.5 * &acceleration * dt2
377 };
378
379 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 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 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 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 let new_position = &state.position + &state.velocity * dt + 0.5 * &acceleration * dt2;
420
421 let new_velocity = &state.velocity + &acceleration * dt;
424 let new_angular_velocity = &state.angular_velocity + &angular_acceleration * dt;
425
426 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 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 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 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 fn central_difference_step(
475 &self,
476 state: &RigidBodyState,
477 forces: &Array1<f64>,
478 torques: &Array1<f64>,
479 ) -> IntegrateResult<RigidBodyState> {
480 self.verlet_step(state, forces, torques)
482 }
483
484 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 fn calculate_angular_acceleration(
504 &self,
505 torques: &Array1<f64>,
506 _state: &RigidBodyState,
507 ) -> Array1<f64> {
508 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 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 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 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 fn integrate_quaternion(
550 &self,
551 quaternion: &Array1<f64>,
552 angular_velocity: &Array1<f64>,
553 dt: f64,
554 ) -> Array1<f64> {
555 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 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 Self::quaternion_multiply(&dq, quaternion)
580 }
581
582 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 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 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 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]; }
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 if violation.is_nan() || !violation.is_finite() || violation > 1e6 {
651 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 if self.properties.constraint_jacobians.len() == self.properties.constraints.len() {
663 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 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 let position_correction = jacobian.t().dot(&delta_lambda);
683
684 let damping = 0.5; 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 if violation > 1e3 {
695 self.project_to_constraint_manifold(state)?;
697 }
698 } else {
699 let correction_factor = 0.01; if num_constraints > 0 && combined_pos.len() >= 4 {
702 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 let correction_factor = 0.01;
713 if !constraint_values.is_empty() && state.position.len() >= 4 {
714 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 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 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 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 let penalty_force = -stiffness * violation;
767
768 constraint_forces[0] += penalty_force;
770 }
771
772 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 fn enforce_augmented_lagrangian_constraints(
783 &mut self,
784 state: &mut RigidBodyState,
785 penalty: f64,
786 ) -> IntegrationResult {
787 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 fn enforce_baumgarte_stabilization(
802 &self,
803 state: &mut RigidBodyState,
804 alpha: f64,
805 beta: f64,
806 ) -> IntegrationResult {
807 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 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 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 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 let mut aug_matrix = Array2::zeros((n, n + 1));
850
851 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 for i in 0..n {
861 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 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 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 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 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 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 fn project_to_constraint_manifold(&self, state: &mut RigidBodyState) -> IntegrateResult<()> {
916 if self.properties.constraints.len() == 2 && state.position.len() >= 6 {
918 let l1 = 1.0; let l2 = 0.8;
921
922 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 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 state.position[0] = 0.0;
936 state.position[1] = -l1;
937 }
938
939 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 state.position[3] = state.position[0];
949 state.position[4] = state.position[1] - l2;
950 }
951
952 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 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 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 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 for i in 0..n_bodies {
987 let mass = self.properties.mass[i];
988 let inertia = &self.properties.inertia[i];
989
990 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 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 let potential_energy = if self.properties.external_forces.is_some() {
1010 if self.properties.mass.len() == 1 && !self.properties.spring_constants.is_empty() {
1011 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 let g = 9.81;
1017 let m1 = self.properties.mass[0];
1018 let m2 = self.properties.mass[1];
1019 let y1 = state.position[1]; let y2 = state.position[4]; 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 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 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
1068pub mod systems {
1070 use super::*;
1071
1072 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 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]), Array1::zeros(3),
1130 );
1131
1132 (config, properties, state)
1133 }
1134
1135 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 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 let constraint1 = Box::new(move |pos: &Array1<f64>| {
1157 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 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 let jacobian1 = Box::new(move |pos: &Array1<f64>| {
1176 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; jac[(0, 1)] = 2.0 * y1; jac
1184 });
1185
1186 let jacobian2 = Box::new(move |pos: &Array1<f64>| {
1187 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; jac[(0, 1)] = -2.0 * dy; jac[(0, 3)] = 2.0 * dx; jac[(0, 4)] = 2.0 * dy; 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 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]), Array1::zeros(6), );
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; 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]), 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, 10.0, 0.1, 1.0, 0.0, );
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, 0.5, 1.0, 0.8, [0.1, 0.2], [0.0, 0.0], );
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 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); }
1349
1350 #[test]
1351 fn test_simple_integration_step() {
1352 let (config, properties, state) = systems::rigid_body(
1353 1.0, Array2::eye(3), Array1::zeros(3), Array1::from_vec(vec![1.0, 0.0, 0.0]), Array1::from_vec(vec![1.0, 0.0, 0.0, 0.0]), Array1::zeros(3), );
1360
1361 let mut integrator = MechanicalIntegrator::new(config, properties);
1362
1363 let result = integrator.step(0.0, &state).unwrap();
1364
1365 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, 10.0, 0.0, 1.0, 0.0, );
1379
1380 config.dt = 0.001; 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 for i in 0..100 {
1388 let result = integrator.step(i as f64 * 0.001, ¤t_state).unwrap();
1389 current_state = result.state;
1390 }
1391
1392 let (relative_drift, max_drift, current_energy) = integrator.energy_statistics();
1393
1394 assert!(
1396 relative_drift < 0.1,
1397 "Energy drift too large: {relative_drift}"
1398 );
1399 }
1400}