use std::f64::consts::PI;
use super::{Bond, FailureMode, PeridynamicMaterial, PeridynamicState};
use crate::pde::peridynamics::neighbor_list::NeighborList;
#[derive(Debug, Clone)]
pub struct BondBasedConfig {
pub horizon: f64,
pub density: f64,
pub youngs_modulus: f64,
pub poissons_ratio: f64,
pub critical_stretch: f64,
}
impl Default for BondBasedConfig {
fn default() -> Self {
Self {
horizon: 0.1,
density: 1.0,
youngs_modulus: 1.0,
poissons_ratio: 1.0 / 3.0,
critical_stretch: 0.04,
}
}
}
#[derive(Debug, Clone)]
pub struct IsotropicMaterial {
pub micro_modulus: f64,
pub critical_stretch: f64,
}
impl IsotropicMaterial {
pub fn new(youngs_modulus: f64, horizon: f64, critical_stretch: f64) -> Self {
let delta4 = horizon * horizon * horizon * horizon;
let micro_modulus = 18.0 * youngs_modulus / (PI * delta4);
Self {
micro_modulus,
critical_stretch,
}
}
pub fn from_fracture_energy(youngs_modulus: f64, horizon: f64, fracture_energy: f64) -> Self {
let critical_stretch =
(5.0 * PI * fracture_energy / (9.0 * youngs_modulus * horizon)).sqrt();
Self::new(youngs_modulus, horizon, critical_stretch)
}
}
impl PeridynamicMaterial for IsotropicMaterial {
fn bond_force(&self, stretch: f64, _xi: [f64; 3]) -> f64 {
self.micro_modulus * stretch
}
fn critical_stretch(&self) -> f64 {
self.critical_stretch
}
}
#[derive(Debug)]
pub struct BondBasedSolver {
pub state: PeridynamicState,
pub volumes: Vec<f64>,
pub masses: Vec<f64>,
pub bonds: Vec<Bond>,
pub initial_bond_counts: Vec<usize>,
pub active_bond_counts: Vec<usize>,
pub material: IsotropicMaterial,
pub config: BondBasedConfig,
pub neighbor_list: NeighborList,
pub prev_forces: Vec<[f64; 3]>,
}
impl BondBasedSolver {
pub fn new(positions: Vec<[f64; 3]>, volumes: Vec<f64>, config: BondBasedConfig) -> Self {
let n = positions.len();
assert_eq!(
volumes.len(),
n,
"volumes must have the same length as positions"
);
let material = IsotropicMaterial::new(
config.youngs_modulus,
config.horizon,
config.critical_stretch,
);
let neighbor_list = NeighborList::build(&positions, config.horizon);
let mut bonds: Vec<Bond> = Vec::new();
for i in 0..n {
for &j in &neighbor_list.neighbors[i] {
if j > i {
let xi = [
positions[j][0] - positions[i][0],
positions[j][1] - positions[i][1],
positions[j][2] - positions[i][2],
];
bonds.push(Bond {
i,
j,
xi,
active: true,
});
}
}
}
let mut initial_bond_counts = vec![0usize; n];
for bond in &bonds {
initial_bond_counts[bond.i] += 1;
initial_bond_counts[bond.j] += 1;
}
let active_bond_counts = initial_bond_counts.clone();
let masses: Vec<f64> = volumes.iter().map(|&v| config.density * v).collect();
let state = PeridynamicState::new(positions);
let prev_forces = vec![[0.0, 0.0, 0.0]; n];
Self {
state,
volumes,
masses,
bonds,
initial_bond_counts,
active_bond_counts,
material,
config,
neighbor_list,
prev_forces,
}
}
pub fn compute_forces(&mut self) -> Vec<[f64; 3]> {
let n = self.state.n_particles;
let mut forces = vec![[0.0_f64; 3]; n];
let cur_pos: Vec<[f64; 3]> = (0..n).map(|i| self.state.current_position(i)).collect();
for bond in self.bonds.iter_mut() {
if !bond.active {
continue;
}
let i = bond.i;
let j = bond.j;
let cy = [
cur_pos[j][0] - cur_pos[i][0],
cur_pos[j][1] - cur_pos[i][1],
cur_pos[j][2] - cur_pos[i][2],
];
let cy_len = (cy[0] * cy[0] + cy[1] * cy[1] + cy[2] * cy[2]).sqrt();
if cy_len < f64::EPSILON {
continue;
}
let xi_len = bond.reference_length();
if xi_len < f64::EPSILON {
continue;
}
let stretch = (cy_len - xi_len) / xi_len;
if stretch > self.material.critical_stretch() {
bond.active = false;
if self.active_bond_counts[i] > 0 {
self.active_bond_counts[i] -= 1;
}
if self.active_bond_counts[j] > 0 {
self.active_bond_counts[j] -= 1;
}
self.state.damage[i] =
Self::compute_damage(self.active_bond_counts[i], self.initial_bond_counts[i]);
self.state.damage[j] =
Self::compute_damage(self.active_bond_counts[j], self.initial_bond_counts[j]);
continue;
}
let f_scalar = self.material.bond_force(stretch, bond.xi);
let n_dir = [cy[0] / cy_len, cy[1] / cy_len, cy[2] / cy_len];
let vj = self.volumes[j];
let vi = self.volumes[i];
forces[i][0] += f_scalar * n_dir[0] * vj;
forces[i][1] += f_scalar * n_dir[1] * vj;
forces[i][2] += f_scalar * n_dir[2] * vj;
forces[j][0] -= f_scalar * n_dir[0] * vi;
forces[j][1] -= f_scalar * n_dir[1] * vi;
forces[j][2] -= f_scalar * n_dir[2] * vi;
}
forces
}
pub fn step(&mut self, dt: f64) {
let n = self.state.n_particles;
let forces_old = self.prev_forces.clone();
for i in 0..n {
let inv_m = 1.0 / self.masses[i];
for d in 0..3 {
self.state.velocities[i][d] += 0.5 * forces_old[i][d] * inv_m * dt;
}
}
for i in 0..n {
for d in 0..3 {
self.state.displacements[i][d] += self.state.velocities[i][d] * dt;
}
}
let forces_new = self.compute_forces();
for i in 0..n {
let inv_m = 1.0 / self.masses[i];
for d in 0..3 {
self.state.velocities[i][d] += 0.5 * forces_new[i][d] * inv_m * dt;
}
}
self.prev_forces = forces_new;
}
#[inline]
fn compute_damage(active: usize, initial: usize) -> f64 {
if initial == 0 {
0.0
} else {
1.0 - (active as f64 / initial as f64)
}
}
pub fn damage_field(&self) -> Vec<f64> {
(0..self.state.n_particles)
.map(|i| Self::compute_damage(self.active_bond_counts[i], self.initial_bond_counts[i]))
.collect()
}
pub fn cfl_timestep(&self) -> f64 {
let n = self.state.n_particles;
let mut min_dt = f64::MAX;
for i in 0..n {
let mut sum_c_vj = 0.0_f64;
for bond in &self.bonds {
if !bond.active {
continue;
}
let j = if bond.i == i {
Some(bond.j)
} else if bond.j == i {
Some(bond.i)
} else {
None
};
if let Some(jj) = j {
sum_c_vj += self.material.micro_modulus * self.volumes[jj];
}
}
if sum_c_vj < f64::EPSILON {
continue;
}
let rho_vi = self.config.density * self.volumes[i];
let dt_i = (rho_vi / sum_c_vj).sqrt();
if dt_i < min_dt {
min_dt = dt_i;
}
}
if min_dt == f64::MAX {
0.0
} else {
0.8 * min_dt
}
}
pub fn bond_active(&self, i: usize, j: usize) -> Option<bool> {
self.bonds
.iter()
.find(|b| (b.i == i && b.j == j) || (b.i == j && b.j == i))
.map(|b| b.active)
}
pub fn failure_mode(&self, i: usize, j: usize) -> FailureMode {
match self
.bonds
.iter()
.find(|b| (b.i == i && b.j == j) || (b.i == j && b.j == i))
{
None => FailureMode::NoBond,
Some(b) if !b.active => FailureMode::StretchExceeded,
_ => {
FailureMode::StretchExceeded }
}
}
pub fn active_bond_count(&self) -> usize {
self.bonds.iter().filter(|b| b.active).count()
}
pub fn kinetic_energy(&self) -> f64 {
(0..self.state.n_particles)
.map(|i| {
let v = &self.state.velocities[i];
let v2 = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
0.5 * self.masses[i] * v2
})
.sum()
}
pub fn strain_energy(&self) -> f64 {
let cur_pos: Vec<[f64; 3]> = (0..self.state.n_particles)
.map(|i| self.state.current_position(i))
.collect();
self.bonds
.iter()
.filter(|b| b.active)
.map(|bond| {
let cy = [
cur_pos[bond.j][0] - cur_pos[bond.i][0],
cur_pos[bond.j][1] - cur_pos[bond.i][1],
cur_pos[bond.j][2] - cur_pos[bond.i][2],
];
let cy_len = (cy[0] * cy[0] + cy[1] * cy[1] + cy[2] * cy[2]).sqrt();
let xi_len = bond.reference_length();
if xi_len < f64::EPSILON || cy_len < f64::EPSILON {
return 0.0;
}
let stretch = (cy_len - xi_len) / xi_len;
let vi = self.volumes[bond.i];
let vj = self.volumes[bond.j];
0.5 * self.material.micro_modulus * stretch * stretch * xi_len * vi * vj
})
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_grid() -> (Vec<[f64; 3]>, Vec<f64>) {
let mut pos = Vec::new();
let mut vol = Vec::new();
let dx = 0.05_f64;
for i in 0_usize..2 {
for j in 0_usize..2 {
for k in 0_usize..2 {
pos.push([i as f64 * dx, j as f64 * dx, k as f64 * dx]);
vol.push(dx * dx * dx);
}
}
}
(pos, vol)
}
#[test]
fn test_zero_displacement_zero_forces() {
let (pos, vol) = simple_grid();
let config = BondBasedConfig {
horizon: 0.08,
..Default::default()
};
let mut solver = BondBasedSolver::new(pos, vol, config);
let forces = solver.compute_forces();
for f in &forces {
for &component in f {
assert!(
component.abs() < 1e-12,
"expected zero force, got {component}"
);
}
}
}
#[test]
fn test_uniform_stretch_consistent() {
let (pos, vol) = simple_grid();
let config = BondBasedConfig {
horizon: 0.08,
..Default::default()
};
let mut solver = BondBasedSolver::new(pos, vol, config);
solver.state.displacements[0][0] = 0.005; let forces = solver.compute_forces();
assert!(
forces[0][0].abs() > 1e-12,
"expected non-zero force on displaced particle, got {}",
forces[0][0]
);
let total_fx: f64 = forces.iter().map(|f| f[0]).sum();
assert!(total_fx.abs() < 1e-10, "total force not zero: {total_fx}");
}
#[test]
fn test_bond_breaks_when_stretch_exceeds_critical() {
let (pos, vol) = simple_grid();
let config = BondBasedConfig {
horizon: 0.08,
critical_stretch: 0.01, ..Default::default()
};
let mut solver = BondBasedSolver::new(pos, vol, config);
solver.state.displacements[0][0] = 0.1;
let _ = solver.compute_forces();
let broken = solver
.bonds
.iter()
.any(|b| !b.active && (b.i == 0 || b.j == 0));
assert!(
broken,
"bonds connected to over-stretched particle should break"
);
}
#[test]
fn test_damage_increases_after_bond_breakage() {
let (pos, vol) = simple_grid();
let config = BondBasedConfig {
horizon: 0.08,
critical_stretch: 0.001, ..Default::default()
};
let mut solver = BondBasedSolver::new(pos, vol, config);
solver.state.displacements[0][0] = 0.05;
let _ = solver.compute_forces();
let damage = solver.damage_field();
assert!(
damage[0] > 0.0,
"damage of over-stretched particle should be > 0, got {}",
damage[0]
);
}
#[test]
fn test_momentum_conservation() {
let (pos, vol) = simple_grid();
let config = BondBasedConfig {
horizon: 0.08,
..Default::default()
};
let mut solver = BondBasedSolver::new(pos, vol, config);
solver.state.displacements[0][0] = 0.002;
solver.state.displacements[3][1] = -0.001;
let forces = solver.compute_forces();
let total: [f64; 3] = forces.iter().fold([0.0; 3], |acc, f| {
[acc[0] + f[0], acc[1] + f[1], acc[2] + f[2]]
});
let tol = 1e-10;
assert!(
total[0].abs() < tol && total[1].abs() < tol && total[2].abs() < tol,
"sum of forces not zero: {total:?}"
);
}
#[test]
fn test_cfl_timestep_positive_and_reasonable() {
let (pos, vol) = simple_grid();
let config = BondBasedConfig {
horizon: 0.08,
..Default::default()
};
let solver = BondBasedSolver::new(pos, vol, config);
let dt = solver.cfl_timestep();
assert!(dt > 0.0, "CFL timestep should be positive, got {dt}");
assert!(
dt < 1.0,
"CFL timestep should be < 1.0 for unit params, got {dt}"
);
}
#[test]
fn test_velocity_verlet_step_preserves_energy_approximately() {
let (pos, vol) = simple_grid();
let config = BondBasedConfig {
horizon: 0.08,
critical_stretch: 1.0, ..Default::default()
};
let mut solver = BondBasedSolver::new(pos, vol, config);
solver.state.velocities[0][0] = 1e-4;
let f0 = solver.compute_forces();
solver.prev_forces = f0;
let cfl = solver.cfl_timestep();
let dt = if cfl > 0.0 { cfl * 0.01_f64 } else { 1e-6_f64 };
let ke0 = solver.kinetic_energy();
let se0 = solver.strain_energy();
let e0 = ke0 + se0;
solver.step(dt);
let e1 = solver.kinetic_energy() + solver.strain_energy();
let abs_err = (e1 - e0).abs();
assert!(
abs_err < (1e-15_f64).max(e0.abs() * 0.05),
"energy changed by {abs_err:.3e} after one micro-step (e0 = {e0:.3e})"
);
}
#[test]
fn test_damage_field_bounded() {
let (pos, vol) = simple_grid();
let config = BondBasedConfig {
horizon: 0.08,
critical_stretch: 0.005,
..Default::default()
};
let mut solver = BondBasedSolver::new(pos, vol, config);
solver.state.displacements[0][0] = 0.1;
let _ = solver.compute_forces();
let damage = solver.damage_field();
for (i, &d) in damage.iter().enumerate() {
assert!((0.0..=1.0).contains(&d), "damage[{i}] = {d} out of [0,1]");
}
}
}