pub fn simp_modulus(rho: f64, e0: f64, e_min: f64, penalty: f64) -> f64 {
e_min + rho.powf(penalty) * (e0 - e_min)
}
pub fn simp_modulus_derivative(rho: f64, e0: f64, e_min: f64, penalty: f64) -> f64 {
penalty * rho.powf(penalty - 1.0) * (e0 - e_min)
}
pub fn simp_compliance_sensitivity(
rho: f64,
e0: f64,
e_min: f64,
penalty: f64,
element_compliance: f64,
) -> f64 {
let e_rho = simp_modulus(rho, e0, e_min, penalty);
if e_rho < 1e-30 {
return 0.0;
}
let de_drho = simp_modulus_derivative(rho, e0, e_min, penalty);
-de_drho / e_rho * element_compliance
}
pub fn simp_oc_update(
rho: f64,
sensitivity_magnitude: f64,
lagrange: f64,
move_limit: f64,
rho_min: f64,
) -> f64 {
if lagrange < 1e-30 {
return rho;
}
let be = (sensitivity_magnitude / lagrange).max(0.0).sqrt();
let rho_trial = rho * be;
rho_trial
.max(rho - move_limit)
.min(rho + move_limit)
.clamp(rho_min, 1.0)
}
pub fn oc_update(
densities: &[f64],
sensitivities: &[f64],
volume_fraction: f64,
move_limit: f64,
rho_min: f64,
) -> Vec<f64> {
assert_eq!(densities.len(), sensitivities.len());
let n = densities.len();
let target = volume_fraction * n as f64;
let mut l1 = 0.0_f64;
let mut l2 = 1e9_f64;
for _ in 0..60 {
let lm = 0.5 * (l1 + l2);
let vol: f64 = densities
.iter()
.zip(sensitivities.iter())
.map(|(&rho, &s)| simp_oc_update(rho, s.abs(), lm, move_limit, rho_min))
.sum();
if vol > target {
l1 = lm;
} else {
l2 = lm;
}
}
let lm = 0.5 * (l1 + l2);
densities
.iter()
.zip(sensitivities.iter())
.map(|(&rho, &s)| simp_oc_update(rho, s.abs(), lm, move_limit, rho_min))
.collect()
}
pub fn volume_fraction(densities: &[f64]) -> f64 {
if densities.is_empty() {
return 0.0;
}
densities.iter().sum::<f64>() / densities.len() as f64
}
pub fn filter_sensitivities_density_weighted(
sensitivities: &[f64],
densities: &[f64],
centroids: &[(f64, f64)],
r_min: f64,
) -> Vec<f64> {
let n = sensitivities.len();
assert_eq!(densities.len(), n);
assert_eq!(centroids.len(), n);
let mut out = vec![0.0_f64; n];
for i in 0..n {
let (xi, yi) = centroids[i];
let mut num = 0.0;
let mut den = 0.0;
for j in 0..n {
let (xj, yj) = centroids[j];
let d = ((xi - xj).powi(2) + (yi - yj).powi(2)).sqrt();
let w = (r_min - d).max(0.0);
num += w * densities[j] * sensitivities[j];
den += w * densities[j];
}
out[i] = if den.abs() > 1e-30 {
num / den
} else {
sensitivities[i]
};
}
out
}
pub fn filter_sensitivities_hat(
sensitivities: &[f64],
centroids: &[(f64, f64)],
r_min: f64,
) -> Vec<f64> {
let n = sensitivities.len();
assert_eq!(centroids.len(), n);
let mut out = vec![0.0_f64; n];
for i in 0..n {
let (xi, yi) = centroids[i];
let mut num = 0.0;
let mut den = 0.0;
for j in 0..n {
let (xj, yj) = centroids[j];
let d = ((xi - xj).powi(2) + (yi - yj).powi(2)).sqrt();
let w = (r_min - d).max(0.0);
num += w * sensitivities[j];
den += w;
}
out[i] = if den.abs() > 1e-30 {
num / den
} else {
sensitivities[i]
};
}
out
}
pub fn heaviside_projection(rho_tilde: f64, beta: f64, eta: f64) -> f64 {
let num = (beta * eta).tanh() + (beta * (rho_tilde - eta)).tanh();
let den = (beta * eta).tanh() + (beta * (1.0 - eta)).tanh();
if den.abs() < 1e-30 {
rho_tilde
} else {
num / den
}
}
pub fn heaviside_derivative(rho_tilde: f64, beta: f64, eta: f64) -> f64 {
let den = (beta * eta).tanh() + (beta * (1.0 - eta)).tanh();
if den.abs() < 1e-30 {
return 1.0;
}
let sech2 = 1.0 - (beta * (rho_tilde - eta)).tanh().powi(2);
beta * sech2 / den
}
pub fn apply_heaviside_projection(densities: &[f64], beta: f64, eta: f64) -> Vec<f64> {
densities
.iter()
.map(|&r| heaviside_projection(r, beta, eta))
.collect()
}
pub fn heaviside_chain_sensitivity(
dc_drho_bar: &[f64],
rho_tilde: &[f64],
beta: f64,
eta: f64,
) -> Vec<f64> {
dc_drho_bar
.iter()
.zip(rho_tilde.iter())
.map(|(&dc, &rt)| dc * heaviside_derivative(rt, beta, eta))
.collect()
}
pub fn enforce_volume_constraint_scaling(densities: &mut [f64], target_vf: f64, rho_min: f64) {
let current = volume_fraction(densities);
if current < 1e-30 {
return;
}
let scale = target_vf / current;
for rho in densities.iter_mut() {
*rho = (*rho * scale).clamp(rho_min, 1.0);
}
}
pub fn volume_constraint_value(densities: &[f64], target_vf: f64) -> f64 {
volume_fraction(densities) / target_vf - 1.0
}
pub fn volume_constraint_gradient(n: usize, target_vf: f64) -> Vec<f64> {
if n == 0 || target_vf < 1e-30 {
return vec![];
}
vec![1.0 / (n as f64 * target_vf); n]
}
pub fn checkerboard_fraction(densities: &[f64], nx: usize, ny: usize) -> f64 {
if nx < 2 || ny < 2 {
return 0.0;
}
let mut checker = 0u32;
let mut total = 0u32;
for row in 0..(ny - 1) {
for col in 0..(nx - 1) {
let a = densities[row * nx + col] > 0.5;
let b = densities[row * nx + col + 1] > 0.5;
let c = densities[(row + 1) * nx + col] > 0.5;
let d = densities[(row + 1) * nx + col + 1] > 0.5;
if (a && !b && !c && d) || (!a && b && c && !d) {
checker += 1;
}
total += 1;
}
}
if total == 0 {
0.0
} else {
checker as f64 / total as f64
}
}
pub fn suppress_checkerboard(
densities: &[f64],
nx: usize,
ny: usize,
w_self: f64,
w_neighbor: f64,
) -> Vec<f64> {
let n = densities.len();
assert_eq!(n, nx * ny);
let mut out = densities.to_vec();
for row in 0..ny {
for col in 0..nx {
let idx = row * nx + col;
let mut sum = w_self * densities[idx];
let mut wt = w_self;
if col > 0 {
sum += w_neighbor * densities[row * nx + col - 1];
wt += w_neighbor;
}
if col + 1 < nx {
sum += w_neighbor * densities[row * nx + col + 1];
wt += w_neighbor;
}
if row > 0 {
sum += w_neighbor * densities[(row - 1) * nx + col];
wt += w_neighbor;
}
if row + 1 < ny {
sum += w_neighbor * densities[(row + 1) * nx + col];
wt += w_neighbor;
}
out[idx] = sum / wt;
}
}
out
}
pub fn penalty_continuation(current: f64, increment: f64, p_max: f64) -> f64 {
(current + increment).min(p_max)
}
pub fn beta_continuation(current_beta: f64, factor: f64, beta_max: f64) -> f64 {
(current_beta * factor).min(beta_max)
}
#[derive(Debug, Clone)]
pub struct ContinuationSchedule {
pub interval: usize,
pub penalty_increment: f64,
pub penalty_max: f64,
pub beta_factor: f64,
pub beta_max: f64,
pub current_penalty: f64,
pub current_beta: f64,
pub iteration: usize,
}
impl ContinuationSchedule {
pub fn new(
interval: usize,
initial_penalty: f64,
penalty_increment: f64,
penalty_max: f64,
initial_beta: f64,
beta_factor: f64,
beta_max: f64,
) -> Self {
Self {
interval,
penalty_increment,
penalty_max,
beta_factor,
beta_max,
current_penalty: initial_penalty,
current_beta: initial_beta,
iteration: 0,
}
}
pub fn step(&mut self) {
self.iteration += 1;
if self.interval > 0 && self.iteration.is_multiple_of(self.interval) {
self.current_penalty = penalty_continuation(
self.current_penalty,
self.penalty_increment,
self.penalty_max,
);
self.current_beta =
beta_continuation(self.current_beta, self.beta_factor, self.beta_max);
}
}
pub fn penalty_saturated(&self) -> bool {
(self.current_penalty - self.penalty_max).abs() < 1e-10
}
pub fn beta_saturated(&self) -> bool {
(self.current_beta - self.beta_max).abs() < 1e-10
}
}
pub fn compliance_objective(
densities: &[f64],
element_compliances: &[f64],
e0: f64,
e_min: f64,
penalty: f64,
) -> f64 {
densities
.iter()
.zip(element_compliances.iter())
.map(|(&rho, &ce)| simp_modulus(rho, e0, e_min, penalty) * ce)
.sum()
}
pub fn compliance_gradient(
densities: &[f64],
element_compliances: &[f64],
e0: f64,
e_min: f64,
penalty: f64,
) -> Vec<f64> {
densities
.iter()
.zip(element_compliances.iter())
.map(|(&rho, &ce)| {
let de = simp_modulus_derivative(rho, e0, e_min, penalty);
-de * ce
})
.collect()
}
pub fn normalised_compliance_gradient(
densities: &[f64],
element_compliances: &[f64],
e0: f64,
e_min: f64,
penalty: f64,
) -> Vec<f64> {
let grad = compliance_gradient(densities, element_compliances, e0, e_min, penalty);
let max_abs = grad.iter().map(|g| g.abs()).fold(0.0_f64, f64::max);
if max_abs < 1e-30 {
return grad;
}
grad.iter().map(|g| g / max_abs).collect()
}
pub fn accumulate_multi_load_sensitivities(
load_sensitivities: &[Vec<f64>],
weights: &[f64],
) -> Vec<f64> {
assert!(!load_sensitivities.is_empty());
assert_eq!(load_sensitivities.len(), weights.len());
let n = load_sensitivities[0].len();
let mut acc = vec![0.0_f64; n];
for (sens, &w) in load_sensitivities.iter().zip(weights.iter()) {
assert_eq!(sens.len(), n);
for (a, &s) in acc.iter_mut().zip(sens.iter()) {
*a += w * s;
}
}
acc
}
pub fn multi_load_compliance(compliances: &[f64], weights: &[f64]) -> f64 {
assert_eq!(compliances.len(), weights.len());
compliances
.iter()
.zip(weights.iter())
.map(|(c, w)| c * w)
.sum()
}
pub fn multi_load_oc_update(
densities: &[f64],
load_sensitivities: &[Vec<f64>],
weights: &[f64],
volume_fraction_target: f64,
move_limit: f64,
rho_min: f64,
) -> Vec<f64> {
let acc_sens = accumulate_multi_load_sensitivities(load_sensitivities, weights);
oc_update(
densities,
&acc_sens,
volume_fraction_target,
move_limit,
rho_min,
)
}
#[derive(Debug, Clone)]
pub struct TopOptIterResult {
pub iteration: usize,
pub compliance: f64,
pub volume_fraction: f64,
pub max_density_change: f64,
pub lagrange: f64,
}
pub fn run_oc_iterations(
initial_densities: &[f64],
sensitivity_fn: &dyn Fn(&[f64]) -> Vec<f64>,
volume_fraction_target: f64,
move_limit: f64,
rho_min: f64,
max_iter: usize,
convergence_tol: f64,
) -> (Vec<f64>, Vec<TopOptIterResult>) {
let n = initial_densities.len();
let mut densities = initial_densities.to_vec();
let mut history = Vec::with_capacity(max_iter);
for iter in 1..=max_iter {
let sens = sensitivity_fn(&densities);
let prev = densities.clone();
densities = oc_update(
&densities,
&sens,
volume_fraction_target,
move_limit,
rho_min,
);
let max_change = densities
.iter()
.zip(prev.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f64, f64::max);
let compliance: f64 = densities
.iter()
.zip(sens.iter())
.map(|(r, s)| r * s.abs())
.sum();
let vf = volume_fraction(&densities);
let lagrange = if n > 0 { compliance / n as f64 } else { 0.0 };
history.push(TopOptIterResult {
iteration: iter,
compliance,
volume_fraction: vf,
max_density_change: max_change,
lagrange,
});
if max_change < convergence_tol {
break;
}
}
(densities, history)
}
pub fn density_filter_and_project(
densities: &[f64],
centroids: &[(f64, f64)],
r_min: f64,
beta: f64,
eta: f64,
) -> Vec<f64> {
let n = densities.len();
assert_eq!(centroids.len(), n);
let mut rho_tilde = vec![0.0_f64; n];
for i in 0..n {
let (xi, yi) = centroids[i];
let mut num = 0.0;
let mut den = 0.0;
for j in 0..n {
let (xj, yj) = centroids[j];
let d = ((xi - xj).powi(2) + (yi - yj).powi(2)).sqrt();
let w = (r_min - d).max(0.0);
num += w * densities[j];
den += w;
}
rho_tilde[i] = if den > 1e-30 { num / den } else { densities[i] };
}
apply_heaviside_projection(&rho_tilde, beta, eta)
}
pub fn gray_scale_measure(densities: &[f64]) -> f64 {
if densities.is_empty() {
return 0.0;
}
let gray = densities.iter().filter(|&&r| r > 0.1 && r < 0.9).count();
gray as f64 / densities.len() as f64
}
pub fn discreteness_measure(densities: &[f64]) -> f64 {
if densities.is_empty() {
return 0.0;
}
let sum: f64 = densities.iter().map(|&r| 4.0 * r * (1.0 - r)).sum();
sum / densities.len() as f64
}
pub fn density_statistics(densities: &[f64]) -> (f64, f64, f64) {
if densities.is_empty() {
return (0.0, 0.0, 0.0);
}
let mean = densities.iter().sum::<f64>() / densities.len() as f64;
let min = densities.iter().cloned().fold(f64::INFINITY, f64::min);
let max = densities.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
(mean, min, max)
}
pub fn von_mises_stress(sigma_x: f64, sigma_y: f64, tau_xy: f64) -> f64 {
(sigma_x * sigma_x - sigma_x * sigma_y + sigma_y * sigma_y + 3.0 * tau_xy * tau_xy).sqrt()
}
pub fn p_norm_aggregation(stresses: &[f64], p: f64, sigma_ref: f64) -> f64 {
if stresses.is_empty() || sigma_ref < 1e-30 {
return 0.0;
}
let sum: f64 = stresses.iter().map(|&s| (s / sigma_ref).powf(p)).sum();
sigma_ref * sum.powf(1.0 / p)
}
pub fn p_norm_sensitivity(stresses: &[f64], p: f64, sigma_ref: f64) -> Vec<f64> {
if stresses.is_empty() || sigma_ref < 1e-30 {
return vec![0.0; stresses.len()];
}
let sum: f64 = stresses.iter().map(|&s| (s / sigma_ref).powf(p)).sum();
if sum < 1e-30 {
return vec![0.0; stresses.len()];
}
let sum_p_m1 = sum.powf((p - 1.0) / p);
stresses
.iter()
.map(|&s| (s / sigma_ref).powf(p - 1.0) / (sigma_ref * sum_p_m1))
.collect()
}
pub fn stress_simp_relaxation(rho: f64, sigma_full: f64, q: f64, epsilon: f64) -> f64 {
let rho_q = rho.powf(q);
let factor = rho_q / (rho_q + epsilon * (1.0 - rho_q));
factor * sigma_full
}
pub fn beso_update(
densities: &[f64],
sensitivities: &[f64],
target_vf: f64,
rho_min: f64,
) -> Vec<f64> {
let n = densities.len();
assert_eq!(sensitivities.len(), n);
let target = (target_vf * n as f64).round() as usize;
let target = target.clamp(1, n);
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&a, &b| {
sensitivities[b]
.partial_cmp(&sensitivities[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut new_dens = vec![rho_min; n];
for &idx in &indices[..target] {
new_dens[idx] = 1.0;
}
new_dens
}
pub fn beso_sensitivity_average(current: &[f64], previous: &[f64]) -> Vec<f64> {
assert_eq!(current.len(), previous.len());
current
.iter()
.zip(previous.iter())
.map(|(c, p)| (c + p) / 2.0)
.collect()
}
pub fn beso_converged(compliance_history: &[f64], window: usize, tol: f64) -> bool {
let n = compliance_history.len();
if n < window * 2 {
return false;
}
let recent: f64 = compliance_history[(n - window)..].iter().sum::<f64>() / window as f64;
let previous: f64 = compliance_history[(n - 2 * window)..(n - window)]
.iter()
.sum::<f64>()
/ window as f64;
if previous.abs() < 1e-30 {
return false;
}
((recent - previous) / previous).abs() < tol
}
pub fn level_set_heaviside(phi: f64, epsilon: f64) -> f64 {
if phi < -epsilon {
0.0
} else if phi > epsilon {
1.0
} else {
0.5 + 3.0 * phi / (4.0 * epsilon) - phi.powi(3) / (4.0 * epsilon.powi(3))
}
}
pub fn level_set_dirac(phi: f64, epsilon: f64) -> f64 {
if phi.abs() <= epsilon {
3.0 / (4.0 * epsilon) * (1.0 - (phi / epsilon).powi(2))
} else {
0.0
}
}
pub fn level_set_advect(phi: &[f64], velocity: &[f64], dt: f64) -> Vec<f64> {
assert_eq!(phi.len(), velocity.len());
phi.iter()
.zip(velocity.iter())
.map(|(&p, &v)| p - v * dt)
.collect()
}
pub fn level_set_reinitialize(phi: &[f64], dx: f64, dt: f64, n_iter: usize) -> Vec<f64> {
let n = phi.len();
let sign: Vec<f64> = phi
.iter()
.map(|&p| {
if p > 1e-14 {
1.0
} else if p < -1e-14 {
-1.0
} else {
0.0
}
})
.collect();
let mut phi_cur = phi.to_vec();
for _ in 0..n_iter {
let mut phi_new = phi_cur.clone();
for i in 1..(n - 1) {
let grad = (phi_cur[i + 1] - phi_cur[i - 1]) / (2.0 * dx);
phi_new[i] += dt * sign[i] * (1.0 - grad.abs());
}
phi_cur = phi_new;
}
phi_cur
}
pub fn level_set_velocity_extension(sensitivities: &[f64]) -> Vec<f64> {
sensitivities.iter().map(|&s| -s).collect()
}
pub fn perimeter_length(densities: &[f64], nx: usize, ny: usize, threshold: f64) -> f64 {
let mut perimeter = 0.0_f64;
for row in 0..ny {
for col in 0..nx {
let is_solid = densities[row * nx + col] >= threshold;
if col + 1 < nx {
let right_solid = densities[row * nx + col + 1] >= threshold;
if is_solid != right_solid {
perimeter += 1.0;
}
}
if row + 1 < ny {
let top_solid = densities[(row + 1) * nx + col] >= threshold;
if is_solid != top_solid {
perimeter += 1.0;
}
}
}
}
perimeter
}
pub fn perimeter_gradient(densities: &[f64], nx: usize, ny: usize, threshold: f64) -> Vec<f64> {
let n = densities.len();
let mut grad = vec![0.0_f64; n];
let neighbours = |row: usize, col: usize| -> Vec<usize> {
let mut nb = Vec::new();
if col > 0 {
nb.push(row * nx + col - 1);
}
if col + 1 < nx {
nb.push(row * nx + col + 1);
}
if row > 0 {
nb.push((row - 1) * nx + col);
}
if row + 1 < ny {
nb.push((row + 1) * nx + col);
}
nb
};
for row in 0..ny {
for col in 0..nx {
let idx = row * nx + col;
let is_solid = densities[idx] >= threshold;
for nb_idx in neighbours(row, col) {
let nb_solid = densities[nb_idx] >= threshold;
if is_solid != nb_solid {
grad[idx] += 1.0;
}
}
}
}
grad
}
pub fn minimum_length_scale_filter(
densities: &[f64],
centroids: &[(f64, f64)],
r_min: f64,
) -> Vec<f64> {
let n = densities.len();
let mut out = vec![0.0_f64; n];
for i in 0..n {
let (xi, yi) = centroids[i];
let mut num = 0.0;
let mut den = 0.0;
for j in 0..n {
let (xj, yj) = centroids[j];
let d = ((xi - xj).powi(2) + (yi - yj).powi(2)).sqrt();
let w = (r_min - d).max(0.0);
num += w * densities[j];
den += w;
}
out[i] = if den > 1e-30 {
(num / den).clamp(0.0, 1.0)
} else {
densities[i]
};
}
out
}
pub fn casting_constraint_projection(densities: &[f64], forward: bool) -> Vec<f64> {
let n = densities.len();
let mut out = densities.to_vec();
if forward {
for i in 1..n {
if out[i] > out[i - 1] {
out[i] = out[i - 1];
}
}
} else {
for i in (0..(n - 1)).rev() {
if out[i] > out[i + 1] {
out[i] = out[i + 1];
}
}
}
out
}
pub fn extrusion_constraint_projection(densities: &[f64], nx: usize, ny: usize) -> Vec<f64> {
assert_eq!(densities.len(), nx * ny);
let mut out = vec![0.0_f64; nx * ny];
for col in 0..nx {
let mean: f64 = (0..ny).map(|row| densities[row * nx + col]).sum::<f64>() / ny as f64;
for row in 0..ny {
out[row * nx + col] = mean;
}
}
out
}
pub fn pareto_weighted_objective(compliance: f64, dynamic_obj: f64, weight: f64) -> f64 {
weight * compliance + (1.0 - weight) * dynamic_obj
}
pub fn thermal_compliance_gradient(
densities: &[f64],
thermal_sensitivities: &[f64],
e0: f64,
e_min: f64,
penalty: f64,
) -> Vec<f64> {
densities
.iter()
.zip(thermal_sensitivities.iter())
.map(|(&rho, &ts)| {
let de = simp_modulus_derivative(rho, e0, e_min, penalty);
-de * ts
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simp_modulus_solid() {
let e = simp_modulus(1.0, 210e9, 1e-3, 3.0);
assert!((e - 210e9).abs() < 1.0);
}
#[test]
fn test_simp_modulus_void() {
let e = simp_modulus(0.0, 210e9, 1e-3, 3.0);
assert!((e - 1e-3).abs() < 1e-10);
}
#[test]
fn test_simp_modulus_half() {
let e = simp_modulus(0.5, 1.0, 0.0, 3.0);
let expected = 0.5_f64.powi(3);
assert!((e - expected).abs() < 1e-12);
}
#[test]
fn test_simp_modulus_derivative_positive() {
let de = simp_modulus_derivative(0.5, 1.0, 0.0, 3.0);
assert!(de > 0.0);
}
#[test]
fn test_simp_compliance_sensitivity_negative() {
let s = simp_compliance_sensitivity(0.5, 210e9, 1e-3, 3.0, 1.0);
assert!(s <= 0.0);
}
#[test]
fn test_oc_update_volume_constraint_satisfied() {
let densities = vec![0.5; 100];
let sensitivities = vec![-1.0; 100];
let new_dens = oc_update(&densities, &sensitivities, 0.4, 0.2, 1e-3);
let vf = volume_fraction(&new_dens);
assert!((vf - 0.4).abs() < 0.05, "vf = {vf}");
}
#[test]
fn test_oc_update_preserves_length() {
let densities = vec![0.5; 50];
let sensitivities = vec![-1.0; 50];
let new_dens = oc_update(&densities, &sensitivities, 0.5, 0.2, 1e-3);
assert_eq!(new_dens.len(), 50);
}
#[test]
fn test_oc_update_density_in_range() {
let densities: Vec<f64> = (0..20).map(|i| 0.1 + i as f64 * 0.04).collect();
let sensitivities: Vec<f64> = (0..20).map(|i| -(i as f64 + 1.0)).collect();
let new_dens = oc_update(&densities, &sensitivities, 0.5, 0.2, 1e-3);
for &rho in &new_dens {
assert!((1e-3..=1.0).contains(&rho), "Out of range: {rho}");
}
}
#[test]
fn test_simp_oc_update_clamps_to_rho_min() {
let rho_new = simp_oc_update(0.001, 0.01, 1e9, 0.2, 1e-3);
assert!(rho_new >= 1e-3);
}
#[test]
fn test_volume_fraction_uniform() {
let dens = vec![0.4; 25];
assert!((volume_fraction(&dens) - 0.4).abs() < 1e-12);
}
fn uniform_centroids(n: usize) -> Vec<(f64, f64)> {
let side = (n as f64).sqrt().ceil() as usize;
(0..n)
.map(|i| ((i % side) as f64 + 0.5, (i / side) as f64 + 0.5))
.collect()
}
#[test]
fn test_filter_density_weighted_length() {
let n = 16;
let sens = vec![-1.0; n];
let dens = vec![0.5; n];
let centroids = uniform_centroids(n);
let out = filter_sensitivities_density_weighted(&sens, &dens, ¢roids, 1.5);
assert_eq!(out.len(), n);
}
#[test]
fn test_filter_density_weighted_uniform_unchanged() {
let n = 9;
let sens = vec![-2.0; n];
let dens = vec![0.5; n];
let centroids = uniform_centroids(n);
let out = filter_sensitivities_density_weighted(&sens, &dens, ¢roids, 2.0);
for &v in &out {
assert!(
(v - (-2.0)).abs() < 1e-6,
"Uniform sensitivity changed: {v}"
);
}
}
#[test]
fn test_filter_hat_length() {
let n = 9;
let sens = vec![-1.0; n];
let centroids = uniform_centroids(n);
let out = filter_sensitivities_hat(&sens, ¢roids, 1.5);
assert_eq!(out.len(), n);
}
#[test]
fn test_filter_hat_uniform_unchanged() {
let n = 9;
let sens = vec![-3.0; n];
let centroids = uniform_centroids(n);
let out = filter_sensitivities_hat(&sens, ¢roids, 1.5);
for &v in &out {
assert!((v - (-3.0)).abs() < 1e-6, "Hat filter changed uniform: {v}");
}
}
#[test]
fn test_heaviside_at_eta() {
let h = heaviside_projection(0.5, 10.0, 0.5);
assert!((h - 0.5).abs() < 1e-6);
}
#[test]
fn test_heaviside_above_eta_near_one() {
let h = heaviside_projection(1.0, 100.0, 0.5);
assert!(h > 0.95, "h = {h}");
}
#[test]
fn test_heaviside_below_eta_near_zero() {
let h = heaviside_projection(0.0, 100.0, 0.5);
assert!(h < 0.05, "h = {h}");
}
#[test]
fn test_heaviside_derivative_positive() {
let dh = heaviside_derivative(0.5, 10.0, 0.5);
assert!(dh > 0.0);
}
#[test]
fn test_heaviside_chain_sensitivity_length() {
let dc = vec![-1.0_f64; 8];
let rt = vec![0.5_f64; 8];
let out = heaviside_chain_sensitivity(&dc, &rt, 10.0, 0.5);
assert_eq!(out.len(), 8);
}
#[test]
fn test_apply_heaviside_projection_length() {
let dens = vec![0.3, 0.5, 0.7, 1.0];
let out = apply_heaviside_projection(&dens, 5.0, 0.5);
assert_eq!(out.len(), 4);
}
#[test]
fn test_enforce_volume_constraint_scaling() {
let mut dens = vec![0.8; 10];
enforce_volume_constraint_scaling(&mut dens, 0.5, 1e-3);
let vf = volume_fraction(&dens);
assert!((vf - 0.5).abs() < 0.01, "vf = {vf}");
}
#[test]
fn test_volume_constraint_value_at_target() {
let dens = vec![0.5; 20];
let g = volume_constraint_value(&dens, 0.5);
assert!(g.abs() < 1e-12, "g = {g}");
}
#[test]
fn test_volume_constraint_value_over_budget() {
let dens = vec![0.8; 20];
let g = volume_constraint_value(&dens, 0.5);
assert!(g > 0.0, "g = {g}");
}
#[test]
fn test_volume_constraint_gradient_length() {
let grad = volume_constraint_gradient(10, 0.5);
assert_eq!(grad.len(), 10);
}
#[test]
fn test_volume_constraint_gradient_uniform() {
let grad = volume_constraint_gradient(10, 0.5);
let expected = 1.0 / (10.0 * 0.5);
for &g in &grad {
assert!((g - expected).abs() < 1e-12);
}
}
#[test]
fn test_checkerboard_fraction_uniform() {
let dens = vec![1.0; 9];
assert_eq!(checkerboard_fraction(&dens, 3, 3), 0.0);
}
#[test]
fn test_checkerboard_fraction_full() {
let dens = vec![1.0, 0.0, 0.0, 1.0];
assert!((checkerboard_fraction(&dens, 2, 2) - 1.0).abs() < 1e-12);
}
#[test]
fn test_suppress_checkerboard_length() {
let dens = vec![1.0, 0.0, 0.0, 1.0];
let out = suppress_checkerboard(&dens, 2, 2, 0.5, 0.125);
assert_eq!(out.len(), 4);
}
#[test]
fn test_suppress_checkerboard_reduces_pattern() {
let dens = vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0];
let out = suppress_checkerboard(&dens, 3, 3, 0.5, 0.125);
let before = checkerboard_fraction(&dens, 3, 3);
let after = checkerboard_fraction(&out, 3, 3);
assert!(after <= before + 1e-10, "before={before} after={after}");
}
#[test]
fn test_penalty_continuation_increments() {
let p = penalty_continuation(3.0, 0.5, 5.0);
assert!((p - 3.5).abs() < 1e-12);
}
#[test]
fn test_penalty_continuation_clamped() {
let p = penalty_continuation(4.9, 0.5, 5.0);
assert!((p - 5.0).abs() < 1e-12);
}
#[test]
fn test_beta_continuation_multiplies() {
let b = beta_continuation(2.0, 3.0, 100.0);
assert!((b - 6.0).abs() < 1e-12);
}
#[test]
fn test_beta_continuation_clamped() {
let b = beta_continuation(50.0, 3.0, 100.0);
assert!((b - 100.0).abs() < 1e-12);
}
#[test]
fn test_continuation_schedule_step() {
let mut sch = ContinuationSchedule::new(5, 3.0, 1.0, 8.0, 1.0, 2.0, 64.0);
for _ in 0..5 {
sch.step();
}
assert_eq!(sch.iteration, 5);
assert!((sch.current_penalty - 4.0).abs() < 1e-12);
assert!((sch.current_beta - 2.0).abs() < 1e-12);
}
#[test]
fn test_continuation_schedule_saturated() {
let mut sch = ContinuationSchedule::new(1, 7.0, 2.0, 8.0, 32.0, 4.0, 64.0);
sch.step(); sch.step(); assert!(sch.penalty_saturated());
assert!(sch.beta_saturated());
}
#[test]
fn test_compliance_objective_positive() {
let dens = vec![0.5; 10];
let ce = vec![1.0; 10];
let c = compliance_objective(&dens, &ce, 1.0, 1e-9, 3.0);
assert!(c > 0.0);
}
#[test]
fn test_compliance_gradient_negative() {
let dens = vec![0.5; 5];
let ce = vec![1.0; 5];
let grad = compliance_gradient(&dens, &ce, 1.0, 1e-9, 3.0);
for &g in &grad {
assert!(g <= 0.0, "Sensitivity should be ≤ 0: {g}");
}
}
#[test]
fn test_normalised_compliance_gradient_max_one() {
let dens = vec![0.3, 0.5, 0.7, 1.0];
let ce = vec![1.0, 2.0, 0.5, 3.0];
let ng = normalised_compliance_gradient(&dens, &ce, 1.0, 1e-9, 3.0);
let max_abs = ng.iter().map(|g| g.abs()).fold(0.0_f64, f64::max);
assert!((max_abs - 1.0).abs() < 1e-12, "max_abs = {max_abs}");
}
#[test]
fn test_compliance_gradient_length() {
let dens = vec![0.5; 8];
let ce = vec![1.0; 8];
let grad = compliance_gradient(&dens, &ce, 1.0, 1e-9, 3.0);
assert_eq!(grad.len(), 8);
}
#[test]
fn test_accumulate_multi_load_sensitivities_length() {
let sens = vec![vec![-1.0; 10], vec![-2.0; 10]];
let w = vec![0.5, 0.5];
let acc = accumulate_multi_load_sensitivities(&sens, &w);
assert_eq!(acc.len(), 10);
}
#[test]
fn test_accumulate_multi_load_sensitivities_values() {
let sens = vec![vec![-1.0; 4], vec![-3.0; 4]];
let w = vec![0.5, 0.5];
let acc = accumulate_multi_load_sensitivities(&sens, &w);
for &a in &acc {
assert!((a - (-2.0)).abs() < 1e-12, "acc = {a}");
}
}
#[test]
fn test_multi_load_compliance() {
let c = multi_load_compliance(&[10.0, 20.0], &[0.6, 0.4]);
assert!((c - 14.0).abs() < 1e-12, "c = {c}");
}
#[test]
fn test_multi_load_oc_update_volume() {
let dens = vec![0.5; 50];
let sens1 = vec![-1.0; 50];
let sens2 = vec![-1.0; 50];
let new_dens = multi_load_oc_update(&dens, &[sens1, sens2], &[0.5, 0.5], 0.4, 0.2, 1e-3);
let vf = volume_fraction(&new_dens);
assert!((vf - 0.4).abs() < 0.05, "vf = {vf}");
}
#[test]
fn test_run_oc_iterations_returns_correct_length() {
let dens = vec![0.5; 16];
let (final_dens, hist) = run_oc_iterations(
&dens,
&|d: &[f64]| d.iter().map(|_| -1.0).collect(),
0.4,
0.2,
1e-3,
5,
1e-4,
);
assert_eq!(final_dens.len(), 16);
assert!(!hist.is_empty());
}
#[test]
fn test_run_oc_iterations_volume_approaches_target() {
let dens = vec![0.8; 25];
let (final_dens, _) = run_oc_iterations(
&dens,
&|d: &[f64]| d.iter().map(|_| -1.0).collect(),
0.5,
0.2,
1e-3,
20,
1e-5,
);
let vf = volume_fraction(&final_dens);
assert!((vf - 0.5).abs() < 0.1, "vf = {vf}");
}
#[test]
fn test_density_filter_and_project_length() {
let dens = vec![0.5; 9];
let centroids = uniform_centroids(9);
let out = density_filter_and_project(&dens, ¢roids, 1.5, 5.0, 0.5);
assert_eq!(out.len(), 9);
}
#[test]
fn test_density_filter_and_project_uniform() {
let dens = vec![0.5; 9];
let centroids = uniform_centroids(9);
let out = density_filter_and_project(&dens, ¢roids, 1.5, 5.0, 0.5);
for &v in &out {
assert!((v - 0.5).abs() < 0.1, "v = {v}");
}
}
#[test]
fn test_gray_scale_all_gray() {
let dens = vec![0.5; 10];
assert!((gray_scale_measure(&dens) - 1.0).abs() < 1e-12);
}
#[test]
fn test_gray_scale_all_binary() {
let dens = vec![0.0, 1.0, 0.0, 1.0];
assert_eq!(gray_scale_measure(&dens), 0.0);
}
#[test]
fn test_discreteness_measure_half() {
let dens = vec![0.5; 4];
let dm = discreteness_measure(&dens);
assert!((dm - 1.0).abs() < 1e-12, "dm = {dm}");
}
#[test]
fn test_discreteness_measure_binary() {
let dens = vec![0.0, 1.0];
let dm = discreteness_measure(&dens);
assert!(dm.abs() < 1e-12, "dm = {dm}");
}
#[test]
fn test_density_statistics_uniform() {
let dens = vec![0.4; 10];
let (mean, min, max) = density_statistics(&dens);
assert!((mean - 0.4).abs() < 1e-12);
assert!((min - 0.4).abs() < 1e-12);
assert!((max - 0.4).abs() < 1e-12);
}
#[test]
fn test_density_statistics_range() {
let dens = vec![0.1, 0.5, 0.9];
let (_mean, min, max) = density_statistics(&dens);
assert!((min - 0.1).abs() < 1e-12);
assert!((max - 0.9).abs() < 1e-12);
}
#[test]
fn test_von_mises_stress_zero_for_zero_stress() {
let svm = von_mises_stress(0.0, 0.0, 0.0);
assert!(svm.abs() < 1e-12);
}
#[test]
fn test_von_mises_stress_uniaxial() {
let svm = von_mises_stress(100.0, 0.0, 0.0);
assert!((svm - 100.0).abs() < 1e-10, "svm={svm}");
}
#[test]
fn test_von_mises_pure_shear() {
let tau = 50.0;
let svm = von_mises_stress(0.0, 0.0, tau);
let expected = (3.0_f64).sqrt() * tau;
assert!(
(svm - expected).abs() < 1e-10,
"svm={svm} expected={expected}"
);
}
#[test]
fn test_p_norm_aggregation_value() {
let stresses = vec![100.0, 200.0, 150.0];
let pn = p_norm_aggregation(&stresses, 8.0, 300.0);
assert!(pn > 0.0, "P-norm should be positive");
}
#[test]
fn test_p_norm_aggregation_approaches_max_high_p() {
let stresses = vec![100.0, 200.0, 50.0];
let pn_high = p_norm_aggregation(&stresses, 100.0, 200.0);
assert!(
(pn_high - 200.0).abs() / 200.0 < 0.05,
"High P-norm should approach max, got {pn_high}"
);
}
#[test]
fn test_p_norm_sensitivity() {
let stresses = vec![100.0, 200.0, 150.0];
let sens = p_norm_sensitivity(&stresses, 8.0, 300.0);
assert_eq!(sens.len(), stresses.len());
for &s in &sens {
assert!(s >= 0.0, "Sensitivity should be non-negative: {s}");
}
}
#[test]
fn test_stress_simp_relaxation() {
let s = stress_simp_relaxation(0.5, 100.0, 3.0, 0.5);
assert!(
s < 100.0,
"Relaxed stress should be less than full stress: {s}"
);
}
#[test]
fn test_stress_simp_relaxation_solid() {
let s = stress_simp_relaxation(1.0, 200.0, 3.0, 0.5);
assert!(
(s - 200.0).abs() < 1e-10,
"At rho=1, relaxed stress should be 200, got {s}"
);
}
#[test]
fn test_beso_update_removes_weakest() {
let sens = vec![0.1, 0.5, 0.2, 0.8, 0.3];
let dens = vec![1.0; 5];
let new_dens = beso_update(&dens, &sens, 0.6, 0.01);
let vf: f64 = new_dens.iter().sum::<f64>() / new_dens.len() as f64;
assert!((vf - 0.6).abs() <= 0.25, "vf={vf}");
}
#[test]
fn test_beso_update_length_preserved() {
let sens = vec![0.1, 0.5, 0.2, 0.8, 0.3];
let dens = vec![1.0; 5];
let new_dens = beso_update(&dens, &sens, 0.6, 0.01);
assert_eq!(new_dens.len(), 5);
}
#[test]
fn test_beso_update_only_binary_densities() {
let sens: Vec<f64> = (0..10).map(|i| i as f64 * 0.1).collect();
let dens = vec![1.0; 10];
let new_dens = beso_update(&dens, &sens, 0.5, 0.01);
for &d in &new_dens {
assert!(
d == 0.01 || d == 1.0,
"BESO should produce binary densities: {d}"
);
}
}
#[test]
fn test_level_set_reinitialize_signed_distance() {
let phi: Vec<f64> = (-5..=5).map(|i| i as f64).collect();
let phi_new = level_set_reinitialize(&phi, 1.0, 0.5, 5);
assert_eq!(phi_new.len(), phi.len());
assert!(
phi_new[5].abs() < 0.5,
"Central value should be near 0: {}",
phi_new[5]
);
}
#[test]
fn test_level_set_heaviside_smooth_transition() {
let eps = 0.1;
let h_neg = level_set_heaviside(-1.0, eps);
let h_zero = level_set_heaviside(0.0, eps);
let h_pos = level_set_heaviside(1.0, eps);
assert!(h_neg < 0.1, "Negative phi should give small H: {h_neg}");
assert!(
(h_zero - 0.5).abs() < 0.1,
"Zero phi should give ~0.5: {h_zero}"
);
assert!(h_pos > 0.9, "Positive phi should give large H: {h_pos}");
}
#[test]
fn test_level_set_sensitivity_to_velocity() {
let sens = vec![-1.0, -2.0, -3.0, -4.0];
let vel = level_set_velocity_extension(&sens);
for &v in &vel {
assert!(v > 0.0, "Velocity should be positive: {v}");
}
}
#[test]
fn test_level_set_update_advances_interface() {
let phi = vec![-1.0, -0.5, 0.0, 0.5, 1.0];
let vel = vec![1.0; 5];
let dt = 0.1;
let phi_new = level_set_advect(&phi, &vel, dt);
assert_eq!(phi_new.len(), phi.len());
for i in 0..phi.len() {
let expected = phi[i] - vel[i] * dt;
assert!(
(phi_new[i] - expected).abs() < 1e-12,
"phi[{i}]={}",
phi_new[i]
);
}
}
#[test]
fn test_perimeter_length_uniform_solid() {
let dens = vec![1.0; 9];
let p = perimeter_length(&dens, 3, 3, 0.5);
assert_eq!(p, 0.0, "Uniform solid has no internal boundary: {p}");
}
#[test]
fn test_perimeter_length_checkerboard_high() {
let dens = vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0];
let p = perimeter_length(&dens, 3, 3, 0.5);
assert!(p > 0.0, "Checkerboard should have nonzero perimeter: {p}");
}
#[test]
fn test_perimeter_gradient_length() {
let dens = vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0];
let g = perimeter_gradient(&dens, 3, 3, 0.5);
assert_eq!(g.len(), 9);
}
#[test]
fn test_minimum_length_scale_filter_length() {
let dens = vec![0.5; 9];
let centroids: Vec<(f64, f64)> = (0..9).map(|i| (i as f64, 0.0)).collect();
let out = minimum_length_scale_filter(&dens, ¢roids, 2.0);
assert_eq!(out.len(), 9);
}
#[test]
fn test_minimum_length_scale_filter_values_in_range() {
let dens: Vec<f64> = (0..16)
.map(|i| if i % 4 == 0 { 1.0 } else { 0.0 })
.collect();
let centroids: Vec<(f64, f64)> =
(0..16).map(|i| ((i % 4) as f64, (i / 4) as f64)).collect();
let out = minimum_length_scale_filter(&dens, ¢roids, 1.5);
for &v in &out {
assert!(
(0.0..=1.0).contains(&v),
"Filtered density out of range: {v}"
);
}
}
#[test]
fn test_casting_constraint_demold_direction() {
let dens = vec![0.8, 0.6, 0.4, 0.2]; let out = casting_constraint_projection(&dens, true);
assert_eq!(out.len(), 4);
for i in 1..out.len() {
assert!(
out[i] <= out[i - 1] + 1e-10,
"Casting constraint violated: out[{i}]={} > out[{}]={}",
out[i],
i - 1,
out[i - 1]
);
}
}
#[test]
fn test_extrusion_constraint_z_uniform() {
let dens = vec![0.8, 0.5, 0.3, 0.8, 0.5, 0.3]; let out = extrusion_constraint_projection(&dens, 3, 2);
assert_eq!(out.len(), 6);
for col in 0..3 {
let v0 = out[col];
let v1 = out[3 + col];
assert!(
(v0 - v1).abs() < 1e-10,
"Extrusion: col {col} should be uniform: {v0} vs {v1}"
);
}
}
#[test]
fn test_pareto_weight_compliance_stiffness() {
let c_comp = 100.0;
let c_freq = 500.0;
let w = 0.3;
let obj = pareto_weighted_objective(c_comp, c_freq, w);
let expected = w * c_comp + (1.0 - w) * c_freq;
assert!((obj - expected).abs() < 1e-12);
}
#[test]
fn test_pareto_weight_zero_gives_compliance() {
let obj = pareto_weighted_objective(100.0, 500.0, 0.0);
assert!((obj - 500.0).abs() < 1e-12);
}
#[test]
fn test_pareto_weight_one_gives_stiffness() {
let obj = pareto_weighted_objective(100.0, 500.0, 1.0);
assert!((obj - 100.0).abs() < 1e-12);
}
#[test]
fn test_thermal_compliance_gradient() {
let dens = vec![0.5; 5];
let thermal_sens = vec![2.0; 5];
let e0 = 1.0;
let e_min = 1e-9;
let p = 3.0;
let grad = thermal_compliance_gradient(&dens, &thermal_sens, e0, e_min, p);
assert_eq!(grad.len(), 5);
for &g in &grad {
assert!(g <= 0.0, "Thermal sensitivity should be ≤ 0: {g}");
}
}
}