pub type ConstraintRecord = (usize, usize, f64);
#[derive(Debug, Clone)]
pub struct ConstraintDebugInfo {
pub particle_a: usize,
pub particle_b: usize,
pub rest_length: f64,
pub current_length: f64,
pub violation: f64,
pub stiffness: f64,
}
impl ConstraintDebugInfo {
pub fn new(
particle_a: usize,
particle_b: usize,
rest_length: f64,
current_length: f64,
stiffness: f64,
) -> Self {
Self {
particle_a,
particle_b,
rest_length,
current_length,
violation: current_length - rest_length,
stiffness,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ConstraintVisualization {
pub infos: Vec<ConstraintDebugInfo>,
pub frame: usize,
}
impl ConstraintVisualization {
pub fn new(frame: usize) -> Self {
Self {
infos: Vec::new(),
frame,
}
}
pub fn push(&mut self, info: ConstraintDebugInfo) {
self.infos.push(info);
}
}
pub fn compute_constraint_violations(
positions: &[[f64; 3]],
constraints: &[ConstraintRecord],
) -> Vec<f64> {
constraints
.iter()
.map(|&(a, b, rest)| {
let pa = positions[a];
let pb = positions[b];
let dx = pa[0] - pb[0];
let dy = pa[1] - pb[1];
let dz = pa[2] - pb[2];
let cur = (dx * dx + dy * dy + dz * dz).sqrt();
cur - rest
})
.collect()
}
pub fn violation_histogram(violations: &[f64], n_bins: usize) -> Vec<usize> {
if violations.is_empty() || n_bins == 0 {
return vec![0; n_bins];
}
let abs_max = violations.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
if abs_max < 1e-30 {
let mut bins = vec![0usize; n_bins];
bins[0] = violations.len();
return bins;
}
let mut bins = vec![0usize; n_bins];
for &v in violations {
let frac = v.abs() / abs_max;
let idx = ((frac * n_bins as f64) as usize).min(n_bins - 1);
bins[idx] += 1;
}
bins
}
pub fn max_violation(violations: &[f64]) -> f64 {
violations.iter().map(|v| v.abs()).fold(0.0_f64, f64::max)
}
pub fn rms_violation(violations: &[f64]) -> f64 {
if violations.is_empty() {
return 0.0;
}
let sum_sq: f64 = violations.iter().map(|v| v * v).sum();
(sum_sq / violations.len() as f64).sqrt()
}
pub fn color_by_violation(violation: f64, max_viol: f64) -> [f64; 3] {
if max_viol <= 0.0 {
return [0.0, 1.0, 0.0];
}
let t = (violation.abs() / max_viol).clamp(0.0, 1.0);
let r = (2.0 * t).min(1.0);
let g = (2.0 * (1.0 - t)).min(1.0);
[r, g, 0.0]
}
pub fn constraint_force_magnitude(violation: f64, stiffness: f64) -> f64 {
stiffness * violation.abs()
}
pub fn energy_density_map(
positions: &[[f64; 3]],
constraints: &[ConstraintRecord],
stiffness: f64,
) -> Vec<f64> {
let mut energy = vec![0.0_f64; positions.len()];
for &(a, b, rest) in constraints {
let pa = positions[a];
let pb = positions[b];
let dx = pa[0] - pb[0];
let dy = pa[1] - pb[1];
let dz = pa[2] - pb[2];
let cur = (dx * dx + dy * dy + dz * dz).sqrt();
let viol = cur - rest;
let e_half = 0.25 * stiffness * viol * viol; energy[a] += e_half;
energy[b] += e_half;
}
energy
}
pub fn convergence_metric(violations_history: &[Vec<f64>]) -> Vec<f64> {
violations_history
.iter()
.map(|frame_violations| rms_violation(frame_violations))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn two_particle_setup() -> (Vec<[f64; 3]>, Vec<ConstraintRecord>) {
let positions = vec![[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]];
let constraints = vec![(0usize, 1usize, 1.0_f64)];
(positions, constraints)
}
#[test]
fn test_violation_exact() {
let (pos, cons) = two_particle_setup();
let viols = compute_constraint_violations(&pos, &cons);
assert_eq!(viols.len(), 1);
assert!((viols[0] - 0.5).abs() < 1e-12, "violation = 0.5");
}
#[test]
fn test_violation_at_rest() {
let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let constraints = vec![(0, 1, 1.0)];
let viols = compute_constraint_violations(&positions, &constraints);
assert!(viols[0].abs() < 1e-12, "at rest → zero violation");
}
#[test]
fn test_violation_compressed() {
let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
let constraints = vec![(0, 1, 1.0)];
let viols = compute_constraint_violations(&positions, &constraints);
assert!(
(viols[0] - (-0.5)).abs() < 1e-12,
"compressed → negative violation"
);
}
#[test]
fn test_violation_empty_constraints() {
let positions = vec![[0.0, 0.0, 0.0]];
let constraints: Vec<ConstraintRecord> = vec![];
let viols = compute_constraint_violations(&positions, &constraints);
assert!(viols.is_empty());
}
#[test]
fn test_violation_multiple_constraints() {
let positions = vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [4.0, 0.0, 0.0]];
let constraints = vec![(0, 1, 1.0), (1, 2, 1.0)];
let viols = compute_constraint_violations(&positions, &constraints);
assert_eq!(viols.len(), 2);
assert!((viols[0] - 1.0).abs() < 1e-12);
assert!((viols[1] - 1.0).abs() < 1e-12);
}
#[test]
fn test_histogram_all_same() {
let viols = vec![1.0, 1.0, 1.0, 1.0];
let hist = violation_histogram(&viols, 4);
let total: usize = hist.iter().sum();
assert_eq!(total, 4, "all 4 violations should appear in histogram");
}
#[test]
fn test_histogram_length() {
let viols = vec![0.1, 0.5, 0.9];
let hist = violation_histogram(&viols, 10);
assert_eq!(hist.len(), 10);
}
#[test]
fn test_histogram_empty() {
let hist = violation_histogram(&[], 5);
assert_eq!(hist.len(), 5);
assert!(hist.iter().all(|&c| c == 0));
}
#[test]
fn test_histogram_zero_bins() {
let hist = violation_histogram(&[1.0, 2.0], 0);
assert!(hist.is_empty());
}
#[test]
fn test_histogram_count_preserved() {
let viols: Vec<f64> = (0..20).map(|i| i as f64 * 0.1).collect();
let hist = violation_histogram(&viols, 5);
let total: usize = hist.iter().sum();
assert_eq!(total, 20);
}
#[test]
fn test_max_violation_basic() {
let v = vec![0.1, -0.5, 0.3, -0.2];
assert!((max_violation(&v) - 0.5).abs() < 1e-12);
}
#[test]
fn test_max_violation_empty() {
assert_eq!(max_violation(&[]), 0.0);
}
#[test]
fn test_max_violation_all_negative() {
let v = vec![-0.3, -0.7, -0.1];
assert!((max_violation(&v) - 0.7).abs() < 1e-12);
}
#[test]
fn test_rms_violation_basic() {
let v = vec![3.0, 4.0];
let expected = (12.5_f64).sqrt();
assert!((rms_violation(&v) - expected).abs() < 1e-12);
}
#[test]
fn test_rms_violation_empty() {
assert_eq!(rms_violation(&[]), 0.0);
}
#[test]
fn test_rms_violation_zero() {
let v = vec![0.0, 0.0, 0.0];
assert_eq!(rms_violation(&v), 0.0);
}
#[test]
fn test_rms_violation_non_negative() {
let v = vec![-1.0, -2.0, -3.0];
assert!(rms_violation(&v) > 0.0);
}
#[test]
fn test_color_zero_violation_is_green() {
let c = color_by_violation(0.0, 1.0);
assert!(c[1] > 0.99 && c[0] < 0.01, "zero violation → green");
}
#[test]
fn test_color_max_violation_is_red() {
let c = color_by_violation(1.0, 1.0);
assert!(c[0] > 0.99 && c[1] < 0.01, "max violation → red");
}
#[test]
fn test_color_channels_in_range() {
for v in [0.0, 0.25, 0.5, 0.75, 1.0] {
let c = color_by_violation(v, 1.0);
for ch in c {
assert!((0.0..=1.0).contains(&ch), "channel out of range");
}
}
}
#[test]
fn test_color_zero_max_viol() {
let c = color_by_violation(0.5, 0.0);
assert!(c[1] > 0.99);
}
#[test]
fn test_force_magnitude_basic() {
let f = constraint_force_magnitude(0.1, 1000.0);
assert!((f - 100.0).abs() < 1e-10);
}
#[test]
fn test_force_magnitude_negative_violation() {
let f = constraint_force_magnitude(-0.2, 500.0);
assert!(
(f - 100.0).abs() < 1e-10,
"absolute value of violation used"
);
}
#[test]
fn test_force_magnitude_zero() {
assert_eq!(constraint_force_magnitude(0.0, 1000.0), 0.0);
}
#[test]
fn test_energy_density_length() {
let positions = vec![[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]];
let constraints = vec![(0usize, 1usize, 1.0_f64)];
let e = energy_density_map(&positions, &constraints, 1000.0);
assert_eq!(e.len(), 2);
}
#[test]
fn test_energy_density_symmetric() {
let positions = vec![[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]];
let constraints = vec![(0, 1, 1.0)];
let e = energy_density_map(&positions, &constraints, 1000.0);
assert!((e[0] - e[1]).abs() < 1e-12, "energy split equally");
}
#[test]
fn test_energy_density_at_rest_zero() {
let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let constraints = vec![(0, 1, 1.0)];
let e = energy_density_map(&positions, &constraints, 1000.0);
assert!(e[0].abs() < 1e-12 && e[1].abs() < 1e-12);
}
#[test]
fn test_energy_density_positive() {
let positions = vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let constraints = vec![(0, 1, 1.0)];
let e = energy_density_map(&positions, &constraints, 1000.0);
assert!(e[0] > 0.0 && e[1] > 0.0);
}
#[test]
fn test_convergence_metric_decreasing() {
let history = vec![
vec![1.0, 1.0, 1.0],
vec![0.5, 0.5, 0.5],
vec![0.1, 0.1, 0.1],
];
let c = convergence_metric(&history);
assert_eq!(c.len(), 3);
assert!(c[0] > c[1] && c[1] > c[2], "should decrease");
}
#[test]
fn test_convergence_metric_empty_history() {
let c = convergence_metric(&[]);
assert!(c.is_empty());
}
#[test]
fn test_convergence_metric_single_frame() {
let history = vec![vec![3.0, 4.0]];
let c = convergence_metric(&history);
assert_eq!(c.len(), 1);
let expected = (12.5_f64).sqrt();
assert!((c[0] - expected).abs() < 1e-12);
}
#[test]
fn test_constraint_visualization_push() {
let mut cv = ConstraintVisualization::new(0);
let info = ConstraintDebugInfo::new(0, 1, 1.0, 1.5, 1000.0);
cv.push(info);
assert_eq!(cv.infos.len(), 1);
assert!((cv.infos[0].violation - 0.5).abs() < 1e-12);
}
#[test]
fn test_constraint_debug_info_violation_sign() {
let info = ConstraintDebugInfo::new(0, 1, 1.0, 0.5, 1000.0);
assert!(
(info.violation - (-0.5)).abs() < 1e-12,
"compressed → negative violation"
);
}
}