#[inline]
fn clamp(v: f64, lo: f64, hi: f64) -> f64 {
v.max(lo).min(hi)
}
#[inline]
fn dist2d(xi: usize, yi: usize, xj: usize, yj: usize) -> f64 {
let dx = xi as f64 - xj as f64;
let dy = yi as f64 - yj as f64;
(dx * dx + dy * dy).sqrt()
}
#[derive(Debug, Clone)]
pub struct DensityField {
pub nx: usize,
pub ny: usize,
pub rho: Vec<f64>,
pub penalty: f64,
pub e0: f64,
pub e_min: f64,
}
impl DensityField {
pub fn new(nx: usize, ny: usize, rho0: f64, penalty: f64, e0: f64, e_min: f64) -> Self {
let n = nx * ny;
Self {
nx,
ny,
rho: vec![rho0.clamp(0.0, 1.0); n],
penalty,
e0,
e_min,
}
}
pub fn num_elements(&self) -> usize {
self.nx * self.ny
}
pub fn effective_modulus(&self, e: usize) -> f64 {
self.e_min + (self.e0 - self.e_min) * self.rho[e].powf(self.penalty)
}
pub fn dmodulus_drho(&self, e: usize) -> f64 {
(self.e0 - self.e_min) * self.penalty * self.rho[e].powf(self.penalty - 1.0)
}
pub fn volume_fraction(&self) -> f64 {
let sum: f64 = self.rho.iter().sum();
sum / self.rho.len() as f64
}
pub fn clamp_densities(&mut self, rho_min: f64, rho_max: f64) {
for r in &mut self.rho {
*r = clamp(*r, rho_min, rho_max);
}
}
pub fn idx(&self, ix: usize, iy: usize) -> usize {
iy * self.nx + ix
}
}
#[derive(Debug, Clone)]
pub struct ComplianceObjective {
pub compliance: f64,
pub sensitivity: Vec<f64>,
}
impl ComplianceObjective {
pub fn new(compliance: f64, sensitivity: Vec<f64>) -> Self {
Self {
compliance,
sensitivity,
}
}
pub fn from_strain_energy(density: &DensityField, ue_ke_ue: &[f64]) -> Self {
assert_eq!(density.num_elements(), ue_ke_ue.len());
let n = density.num_elements();
let mut compliance = 0.0_f64;
let mut sensitivity = vec![0.0_f64; n];
for e in 0..n {
let rho = density.rho[e].max(1e-12);
compliance += density.effective_modulus(e) / density.e0 * ue_ke_ue[e];
sensitivity[e] = -(density.penalty / rho) * ue_ke_ue[e];
}
Self {
compliance,
sensitivity,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct VolumeConstraint {
pub v_target: f64,
}
impl VolumeConstraint {
pub fn new(v_target: f64) -> Self {
Self {
v_target: v_target.clamp(1e-4, 1.0),
}
}
pub fn violation(&self, density: &DensityField) -> f64 {
density.volume_fraction() - self.v_target
}
pub fn is_satisfied(&self, density: &DensityField, tol: f64) -> bool {
self.violation(density).abs() <= tol
}
pub fn bisect_lambda(
&self,
density: &DensityField,
sensitivity: &[f64],
move_limit: f64,
) -> f64 {
let mut lo = 1e-10_f64;
let mut hi = 1e10_f64;
for _ in 0..100 {
let mid = 0.5 * (lo + hi);
let vf = trial_volume(density, sensitivity, mid, move_limit);
if vf > self.v_target {
lo = mid;
} else {
hi = mid;
}
if (hi - lo) / (lo + hi + 1e-30) < 1e-12 {
break;
}
}
0.5 * (lo + hi)
}
}
fn trial_volume(density: &DensityField, sensitivity: &[f64], lambda: f64, move_limit: f64) -> f64 {
let n = density.num_elements();
let sum: f64 = density
.rho
.iter()
.zip(sensitivity.iter())
.map(|(&rho, &sens)| {
let s = (-sens / lambda).sqrt();
clamp(s * rho, rho - move_limit, rho + move_limit).clamp(1e-3, 1.0)
})
.sum();
sum / n as f64
}
#[derive(Debug, Clone, Copy)]
pub struct OcMethod {
pub move_limit: f64,
pub rho_min: f64,
}
impl OcMethod {
pub fn new(move_limit: f64, rho_min: f64) -> Self {
Self {
move_limit,
rho_min,
}
}
pub fn update(&self, density: &mut DensityField, sensitivity: &[f64], lambda: f64) -> Vec<f64> {
let n = density.num_elements();
let mut new_rho = vec![0.0_f64; n];
for e in 0..n {
let rho = density.rho[e];
let b = (-sensitivity[e] / lambda).max(0.0).sqrt();
let rho_new = clamp(b * rho, rho - self.move_limit, rho + self.move_limit)
.clamp(self.rho_min, 1.0);
new_rho[e] = rho_new;
density.rho[e] = rho_new;
}
new_rho
}
pub fn max_change(old_rho: &[f64], new_rho: &[f64]) -> f64 {
old_rho
.iter()
.zip(new_rho.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f64, f64::max)
}
}
#[derive(Debug, Clone, Copy)]
pub struct FilterSensitivity {
pub r_min: f64,
}
impl FilterSensitivity {
pub fn new(r_min: f64) -> Self {
Self { r_min }
}
pub fn apply(&self, density: &DensityField, sensitivity: &[f64]) -> Vec<f64> {
let nx = density.nx;
let ny = density.ny;
let n = nx * ny;
let mut filtered = vec![0.0_f64; n];
let r_ceil = self.r_min.ceil() as usize;
for iy in 0..ny {
for ix in 0..nx {
let e = density.idx(ix, iy);
let mut num = 0.0_f64;
let mut denom = 0.0_f64;
let iy_lo = iy.saturating_sub(r_ceil);
let iy_hi = (iy + r_ceil + 1).min(ny);
let ix_lo = ix.saturating_sub(r_ceil);
let ix_hi = (ix + r_ceil + 1).min(nx);
for jy in iy_lo..iy_hi {
for jx in ix_lo..ix_hi {
let d = dist2d(ix, iy, jx, jy);
if d < self.r_min {
let w = self.r_min - d;
let i = density.idx(jx, jy);
num += w * density.rho[i] * sensitivity[i];
denom += w * density.rho[i];
}
}
}
filtered[e] = if denom.abs() < 1e-30 {
sensitivity[e]
} else {
num / (density.rho[e].max(1e-12) * denom) * density.rho[e]
};
}
}
filtered
}
}
#[derive(Debug, Clone, Default)]
pub struct TopOptHistory {
pub compliance: Vec<f64>,
pub volume: Vec<f64>,
pub max_change: Vec<f64>,
}
impl TopOptHistory {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, compliance: f64, volume: f64, max_change: f64) {
self.compliance.push(compliance);
self.volume.push(volume);
self.max_change.push(max_change);
}
pub fn len(&self) -> usize {
self.compliance.len()
}
pub fn is_empty(&self) -> bool {
self.compliance.is_empty()
}
pub fn is_converged(&self, tol: f64) -> bool {
self.max_change.last().is_some_and(|&c| c < tol)
}
pub fn compliance_improvement(&self, window: usize) -> f64 {
let n = self.compliance.len();
if n < 2 {
return f64::MAX;
}
let w = window.min(n - 1);
let old = self.compliance[n - 1 - w];
let new = self.compliance[n - 1];
(old - new).abs()
}
}
#[derive(Debug, Clone, Copy)]
pub struct HeavisideProjection {
pub beta: f64,
pub eta: f64,
}
impl HeavisideProjection {
pub fn new(beta: f64, eta: f64) -> Self {
Self {
beta,
eta: eta.clamp(1e-4, 1.0 - 1e-4),
}
}
pub fn project(&self, rho: f64) -> f64 {
let b = self.beta;
let e = self.eta;
let denom = (b * e).tanh() + (b * (1.0 - e)).tanh();
if denom.abs() < 1e-30 {
return rho;
}
((b * e).tanh() + (b * (rho - e)).tanh()) / denom
}
pub fn derivative(&self, rho: f64) -> f64 {
let b = self.beta;
let e = self.eta;
let denom = (b * e).tanh() + (b * (1.0 - e)).tanh();
if denom.abs() < 1e-30 {
return 1.0;
}
let sech2 = 1.0 - (b * (rho - e)).tanh().powi(2);
b * sech2 / denom
}
pub fn apply_field(&self, density: &mut DensityField) {
for r in &mut density.rho {
*r = self.project(*r);
}
}
pub fn increase_beta(&mut self, factor: f64) {
self.beta *= factor;
}
}
#[derive(Debug, Clone, Copy)]
pub struct ManufacturingConstraint {
pub min_length_scale: f64,
pub max_overhang_deg: f64,
}
impl ManufacturingConstraint {
pub fn new(min_length_scale: f64, max_overhang_deg: f64) -> Self {
Self {
min_length_scale,
max_overhang_deg: max_overhang_deg.clamp(0.0, 90.0),
}
}
pub fn enforce_min_length(&self, density: &mut DensityField, threshold: f64) -> usize {
let nx = density.nx;
let ny = density.ny;
let n = nx * ny;
let r = self.min_length_scale as usize + 1;
let binary: Vec<f64> = density
.rho
.iter()
.map(|&v| if v >= threshold { 1.0 } else { 0.0 })
.collect();
let mut eroded = vec![0.0_f64; n];
for iy in 0..ny {
for ix in 0..nx {
let mut all_solid = true;
'outer: for jy in iy.saturating_sub(r)..((iy + r + 1).min(ny)) {
for jx in ix.saturating_sub(r)..((ix + r + 1).min(nx)) {
if dist2d(ix, iy, jx, jy) <= self.min_length_scale
&& binary[jy * nx + jx] < 0.5
{
all_solid = false;
break 'outer;
}
}
}
eroded[iy * nx + ix] = if all_solid { 1.0 } else { 0.0 };
}
}
let mut dilated = vec![0.0_f64; n];
for iy in 0..ny {
for ix in 0..nx {
let mut any_solid = false;
'outer2: for jy in iy.saturating_sub(r)..((iy + r + 1).min(ny)) {
for jx in ix.saturating_sub(r)..((ix + r + 1).min(nx)) {
if dist2d(ix, iy, jx, jy) <= self.min_length_scale
&& eroded[jy * nx + jx] > 0.5
{
any_solid = true;
break 'outer2;
}
}
}
dilated[iy * nx + ix] = if any_solid { 1.0 } else { 0.0 };
}
}
let count = dilated
.iter()
.zip(density.rho.iter())
.filter(|&(&d, &r)| (d - r).abs() > 0.5)
.count();
for (rho, &d) in density.rho.iter_mut().zip(dilated.iter()) {
*rho = d;
}
count
}
pub fn enforce_overhang(&self, density: &mut DensityField, threshold: f64) -> usize {
let nx = density.nx;
let ny = density.ny;
let max_tan = self.max_overhang_deg.to_radians().tan();
let mut count = 0;
for iy in 1..ny {
for ix in 0..nx {
let e = density.idx(ix, iy);
if density.rho[e] < threshold {
continue;
}
let support = if ix > 0 {
density.rho[density.idx(ix - 1, iy - 1)]
.max(density.rho[density.idx(ix, iy - 1)])
} else {
density.rho[density.idx(ix, iy - 1)]
};
if support < threshold {
let span_x = 1.0_f64; let span_y = 1.0_f64; if span_x / span_y > max_tan {
density.rho[e] *= 0.5; count += 1;
}
}
}
}
count
}
}
#[derive(Debug, Clone)]
pub struct TopOptSolver {
pub oc: OcMethod,
pub filter: FilterSensitivity,
pub volume: VolumeConstraint,
pub tol: f64,
pub max_iter: usize,
}
impl TopOptSolver {
pub fn new(v_target: f64, r_min: f64, move_limit: f64, tol: f64, max_iter: usize) -> Self {
Self {
oc: OcMethod::new(move_limit, 1e-3),
filter: FilterSensitivity::new(r_min),
volume: VolumeConstraint::new(v_target),
tol,
max_iter,
}
}
pub fn optimise<F>(&self, density: &mut DensityField, mut fea_fn: F) -> TopOptHistory
where
F: FnMut(&DensityField) -> (f64, Vec<f64>),
{
let mut history = TopOptHistory::new();
let mut prev_rho = density.rho.clone();
for _iter in 0..self.max_iter {
let (compliance, ue_ke_ue) = fea_fn(density);
let obj = ComplianceObjective::from_strain_energy(density, &ue_ke_ue);
let filtered_sens = self.filter.apply(density, &obj.sensitivity);
let lambda = self
.volume
.bisect_lambda(density, &filtered_sens, self.oc.move_limit);
let new_rho = self.oc.update(density, &filtered_sens, lambda);
let max_chg = OcMethod::max_change(&prev_rho, &new_rho);
let vf = density.volume_fraction();
history.push(compliance, vf, max_chg);
prev_rho = new_rho;
if max_chg < self.tol {
break;
}
}
history
}
}
pub struct TopOptVisualizer;
impl TopOptVisualizer {
pub fn to_grayscale(density: &DensityField) -> Vec<u8> {
let mut pixels = Vec::with_capacity(density.rho.len());
for iy in (0..density.ny).rev() {
for ix in 0..density.nx {
let e = density.idx(ix, iy);
let grey = (density.rho[e].clamp(0.0, 1.0) * 255.0).round() as u8;
pixels.push(grey);
}
}
pixels
}
pub fn to_ascii(density: &DensityField, threshold: f64) -> String {
let mut out = String::with_capacity(density.nx * density.ny + density.ny);
for iy in (0..density.ny).rev() {
for ix in 0..density.nx {
let e = density.idx(ix, iy);
out.push(if density.rho[e] >= threshold {
'#'
} else {
' '
});
}
out.push('\n');
}
out
}
pub fn to_pgm_bytes(density: &DensityField) -> Vec<u8> {
let header = format!("P5\n{} {}\n255\n", density.nx, density.ny);
let mut out: Vec<u8> = header.into_bytes();
out.extend(Self::to_grayscale(density));
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_density_field_uniform_init() {
let df = DensityField::new(10, 10, 0.5, 3.0, 1.0, 1e-3);
assert_eq!(df.num_elements(), 100);
for &r in &df.rho {
assert!((r - 0.5).abs() < 1e-12);
}
}
#[test]
fn test_effective_modulus_solid() {
let mut df = DensityField::new(2, 2, 1.0, 3.0, 200e9, 1e3);
df.rho[0] = 1.0;
assert!((df.effective_modulus(0) - 200e9).abs() < 1.0);
}
#[test]
fn test_effective_modulus_void() {
let mut df = DensityField::new(2, 2, 0.0, 3.0, 200e9, 1e3);
df.rho[0] = 0.0;
assert!((df.effective_modulus(0) - 1e3).abs() < 1.0);
}
#[test]
fn test_simp_monotone() {
let mut df = DensityField::new(1, 1, 0.0, 3.0, 1.0, 0.0);
df.rho[0] = 0.0;
let mut prev = df.effective_modulus(0);
for i in 1..=10 {
df.rho[0] = i as f64 / 10.0;
let curr = df.effective_modulus(0);
assert!(
curr >= prev,
"Modulus not monotone at rho={}: curr={curr} prev={prev}",
df.rho[0]
);
prev = curr;
}
}
#[test]
fn test_volume_fraction_uniform() {
let df = DensityField::new(4, 5, 0.4, 3.0, 1.0, 0.0);
assert!((df.volume_fraction() - 0.4).abs() < 1e-12);
}
#[test]
fn test_idx() {
let df = DensityField::new(5, 5, 0.5, 3.0, 1.0, 0.0);
assert_eq!(df.idx(2, 3), 3 * 5 + 2);
}
#[test]
fn test_clamp_densities() {
let mut df = DensityField::new(3, 3, 0.5, 3.0, 1.0, 0.0);
df.rho[0] = -0.2;
df.rho[1] = 1.8;
df.clamp_densities(0.0, 1.0);
assert!((df.rho[0] - 0.0).abs() < 1e-12);
assert!((df.rho[1] - 1.0).abs() < 1e-12);
}
#[test]
fn test_dmodulus_drho_positive() {
let mut df = DensityField::new(1, 1, 0.5, 3.0, 1.0, 0.0);
df.rho[0] = 0.5;
assert!(df.dmodulus_drho(0) > 0.0);
}
#[test]
fn test_sensitivity_sign() {
let df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 1e-6);
let ue_ke_ue = vec![1.0; 16];
let obj = ComplianceObjective::from_strain_energy(&df, &ue_ke_ue);
for &s in &obj.sensitivity {
assert!(s < 0.0, "Sensitivity should be negative: {s}");
}
}
#[test]
fn test_compliance_nonneg() {
let df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 1e-6);
let ue_ke_ue = vec![2.0; 16];
let obj = ComplianceObjective::from_strain_energy(&df, &ue_ke_ue);
assert!(obj.compliance >= 0.0);
}
#[test]
fn test_zero_strain_energy() {
let df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
let ue_ke_ue = vec![0.0; 16];
let obj = ComplianceObjective::from_strain_energy(&df, &ue_ke_ue);
assert!(obj.compliance.abs() < 1e-10);
}
#[test]
fn test_volume_violation_zero() {
let vc = VolumeConstraint::new(0.5);
let df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
assert!(vc.violation(&df).abs() < 1e-10);
}
#[test]
fn test_volume_not_satisfied() {
let vc = VolumeConstraint::new(0.3);
let df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
assert!(!vc.is_satisfied(&df, 0.01));
}
#[test]
fn test_bisect_lambda_positive() {
let vc = VolumeConstraint::new(0.4);
let df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
let sensitivity = vec![-1.0; 16];
let lambda = vc.bisect_lambda(&df, &sensitivity, 0.2);
assert!(lambda > 0.0);
}
#[test]
fn test_oc_move_limit() {
let oc = OcMethod::new(0.2, 1e-3);
let mut df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
let old_rho = df.rho.clone();
let sensitivity = vec![-1.0; 16];
let lambda = 1.0;
let new_rho = oc.update(&mut df, &sensitivity, lambda);
for (o, n) in old_rho.iter().zip(new_rho.iter()) {
assert!(
(o - n).abs() <= 0.2 + 1e-12,
"Move limit violated: |{o} - {n}| > 0.2"
);
}
}
#[test]
fn test_oc_density_bounds() {
let oc = OcMethod::new(0.5, 1e-3);
let mut df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
let sensitivity = vec![-0.001; 16];
let lambda = 1e-8; let new_rho = oc.update(&mut df, &sensitivity, lambda);
for &r in &new_rho {
assert!(r <= 1.0 + 1e-12, "Density exceeded 1: {r}");
assert!(r >= 1e-3 - 1e-12, "Density below rho_min: {r}");
}
}
#[test]
fn test_max_change() {
let old = vec![0.5, 0.3, 0.7];
let new = vec![0.6, 0.3, 0.5];
let mc = OcMethod::max_change(&old, &new);
assert!((mc - 0.2).abs() < 1e-12);
}
#[test]
fn test_filter_length() {
let flt = FilterSensitivity::new(1.5);
let df = DensityField::new(5, 5, 0.5, 3.0, 1.0, 0.0);
let sens = vec![-1.0; 25];
let filtered = flt.apply(&df, &sens);
assert_eq!(filtered.len(), 25);
}
#[test]
fn test_filter_uniform() {
let flt = FilterSensitivity::new(2.0);
let df = DensityField::new(8, 8, 0.5, 3.0, 1.0, 0.0);
let sens = vec![-2.0; 64];
let filtered = flt.apply(&df, &sens);
for &f in &filtered {
assert!(
(f - (-2.0)).abs() < 1e-8,
"Uniform field should stay uniform, got {f}"
);
}
}
#[test]
fn test_filter_r_min_small() {
let flt = FilterSensitivity::new(0.4); let df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
let sens: Vec<f64> = (0..16).map(|i| -(i as f64)).collect();
let filtered = flt.apply(&df, &sens);
for (i, (&s, &f)) in sens.iter().zip(filtered.iter()).enumerate() {
assert!((f - s).abs() < 1e-8, "Element {i}: expected {s}, got {f}");
}
}
#[test]
fn test_history_empty_not_converged() {
let h = TopOptHistory::new();
assert!(!h.is_converged(1e-3));
assert!(h.is_empty());
}
#[test]
fn test_history_converged() {
let mut h = TopOptHistory::new();
h.push(1.0, 0.5, 1e-4);
assert!(h.is_converged(1e-3));
}
#[test]
fn test_history_not_converged() {
let mut h = TopOptHistory::new();
h.push(1.0, 0.5, 0.1);
assert!(!h.is_converged(1e-3));
}
#[test]
fn test_compliance_improvement_single() {
let mut h = TopOptHistory::new();
h.push(5.0, 0.4, 0.1);
assert_eq!(h.compliance_improvement(3), f64::MAX);
}
#[test]
fn test_compliance_improvement_multi() {
let mut h = TopOptHistory::new();
h.push(10.0, 0.5, 0.1);
h.push(8.0, 0.5, 0.05);
h.push(7.0, 0.5, 0.02);
assert!((h.compliance_improvement(1) - 1.0).abs() < 1e-10);
}
#[test]
fn test_heaviside_zero() {
let h = HeavisideProjection::new(50.0, 0.5);
let val = h.project(0.0);
assert!(val < 0.05, "H(0) should be near 0, got {val}");
}
#[test]
fn test_heaviside_one() {
let h = HeavisideProjection::new(50.0, 0.5);
let val = h.project(1.0);
assert!(val > 0.95, "H(1) should be near 1, got {val}");
}
#[test]
fn test_heaviside_midpoint() {
let eta = 0.5;
let h = HeavisideProjection::new(10.0, eta);
let val = h.project(eta);
assert!((val - 0.5).abs() < 1e-6, "H(eta) should be 0.5, got {val}");
}
#[test]
fn test_heaviside_derivative_nonneg() {
let h = HeavisideProjection::new(8.0, 0.5);
for i in 0..=10 {
let rho = i as f64 / 10.0;
assert!(h.derivative(rho) >= 0.0);
}
}
#[test]
fn test_increase_beta() {
let mut h = HeavisideProjection::new(2.0, 0.5);
h.increase_beta(3.0);
assert!((h.beta - 6.0).abs() < 1e-12);
}
#[test]
fn test_heaviside_apply_field_bounded() {
let h = HeavisideProjection::new(10.0, 0.5);
let mut df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
h.apply_field(&mut df);
for &r in &df.rho {
assert!(
(0.0..=1.0).contains(&r),
"Projected density out of bounds: {r}"
);
}
}
#[test]
fn test_manufacturing_no_panic() {
let mc = ManufacturingConstraint::new(1.5, 45.0);
let mut df = DensityField::new(6, 6, 0.7, 3.0, 1.0, 0.0);
let _changes = mc.enforce_min_length(&mut df, 0.5);
for &r in &df.rho {
assert!(r == 0.0 || r == 1.0, "Result should be binary: {r}");
}
}
#[test]
fn test_overhang_no_panic() {
let mc = ManufacturingConstraint::new(1.0, 45.0);
let mut df = DensityField::new(5, 5, 0.8, 3.0, 1.0, 0.0);
let _count = mc.enforce_overhang(&mut df, 0.5);
}
#[test]
fn test_overhang_angle_clamped() {
let mc = ManufacturingConstraint::new(1.0, 120.0);
assert_eq!(mc.max_overhang_deg, 90.0);
}
#[test]
fn test_solver_trivial_fea() {
let mut df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 1e-6);
let solver = TopOptSolver::new(0.4, 1.5, 0.2, 1e-3, 20);
let history = solver.optimise(&mut df, |_density| {
let n = 16;
(1.0, vec![1.0; n])
});
assert!(
!history.is_empty(),
"History should have at least one entry"
);
}
#[test]
fn test_solver_volume_target() {
let target = 0.4;
let mut df = DensityField::new(6, 6, 0.6, 3.0, 1.0, 1e-6);
let solver = TopOptSolver::new(target, 1.5, 0.2, 1e-4, 50);
let _history = solver.optimise(&mut df, |d| {
let n = d.num_elements();
let ue_ke_ue: Vec<f64> = (0..n).map(|e| d.effective_modulus(e)).collect();
let compliance: f64 = ue_ke_ue.iter().sum();
(compliance, ue_ke_ue)
});
let vf = df.volume_fraction();
assert!(
(vf - target).abs() < 0.1,
"Volume fraction {vf} should be near target {target}"
);
}
#[test]
fn test_visualizer_grayscale_length() {
let df = DensityField::new(8, 6, 0.5, 3.0, 1.0, 0.0);
let pixels = TopOptVisualizer::to_grayscale(&df);
assert_eq!(pixels.len(), 48);
}
#[test]
fn test_visualizer_solid_white() {
let df = DensityField::new(4, 4, 1.0, 3.0, 1.0, 0.0);
let pixels = TopOptVisualizer::to_grayscale(&df);
assert!(pixels.iter().all(|&p| p == 255));
}
#[test]
fn test_visualizer_void_black() {
let df = DensityField::new(4, 4, 0.0, 3.0, 1.0, 0.0);
let pixels = TopOptVisualizer::to_grayscale(&df);
assert!(pixels.iter().all(|&p| p == 0));
}
#[test]
fn test_visualizer_ascii_line_count() {
let df = DensityField::new(5, 3, 0.5, 3.0, 1.0, 0.0);
let ascii = TopOptVisualizer::to_ascii(&df, 0.5);
assert_eq!(ascii.lines().count(), 3);
}
#[test]
fn test_visualizer_ascii_chars() {
let mut df = DensityField::new(2, 1, 0.0, 3.0, 1.0, 0.0);
df.rho[0] = 1.0;
df.rho[1] = 0.0;
let ascii = TopOptVisualizer::to_ascii(&df, 0.5);
assert!(ascii.contains('#'));
assert!(ascii.contains(' '));
}
#[test]
fn test_visualizer_pgm_header() {
let df = DensityField::new(4, 4, 0.5, 3.0, 1.0, 0.0);
let pgm = TopOptVisualizer::to_pgm_bytes(&df);
assert!(pgm.starts_with(b"P5"), "PGM must start with P5 header");
}
#[test]
fn test_oc_volume_converge() {
let target = 0.35;
let mut df = DensityField::new(10, 10, 0.5, 3.0, 1.0, 1e-6);
let vc = VolumeConstraint::new(target);
let oc = OcMethod::new(0.2, 1e-3);
let sensitivity = vec![-1.0; 100];
for _ in 0..100 {
let lambda = vc.bisect_lambda(&df, &sensitivity, 0.2);
oc.update(&mut df, &sensitivity, lambda);
}
let vf = df.volume_fraction();
assert!(
(vf - target).abs() < 0.02,
"Volume fraction {vf} should approach target {target}"
);
}
#[test]
fn test_volume_fraction_all_ones() {
let df = DensityField::new(5, 5, 1.0, 3.0, 1.0, 0.0);
assert!((df.volume_fraction() - 1.0).abs() < 1e-12);
}
#[test]
fn test_volume_fraction_all_zeros() {
let df = DensityField::new(5, 5, 0.0, 3.0, 1.0, 0.0);
assert!(df.volume_fraction().abs() < 1e-12);
}
#[test]
fn test_filter_preserves_sign() {
let flt = FilterSensitivity::new(1.5);
let df = DensityField::new(5, 5, 0.5, 3.0, 1.0, 0.0);
let sens = vec![-1.0; 25];
let filtered = flt.apply(&df, &sens);
for &f in &filtered {
assert!(f <= 0.0, "Filtered sensitivity should be non-positive: {f}");
}
}
#[test]
fn test_heaviside_monotone() {
let h = HeavisideProjection::new(8.0, 0.5);
let mut prev = h.project(0.0);
for i in 1..=20 {
let rho = i as f64 / 20.0;
let curr = h.project(rho);
assert!(curr >= prev - 1e-12, "Heaviside must be monotone");
prev = curr;
}
}
#[test]
fn test_simp_derivative_p1() {
let e0 = 100.0;
let e_min = 0.1;
let mut df = DensityField::new(1, 1, 0.5, 1.0, e0, e_min);
df.rho[0] = 0.7; let expected = e0 - e_min;
assert!((df.dmodulus_drho(0) - expected).abs() < 1e-8);
}
#[test]
fn test_history_len() {
let mut h = TopOptHistory::new();
for i in 0..7 {
h.push(i as f64, 0.5, 0.01 * i as f64);
}
assert_eq!(h.len(), 7);
}
#[test]
fn test_solver_no_update_at_max_density() {
let mut df = DensityField::new(4, 4, 1.0, 3.0, 1.0, 1e-6);
let solver = TopOptSolver::new(1.0, 1.5, 0.0, 1e-8, 5);
let _h = solver.optimise(&mut df, |_d| (1.0, vec![1.0; 16]));
for &r in &df.rho {
assert!(
(r - 1.0).abs() < 0.01,
"Dense field with zero move limit should stay near 1, got {r}"
);
}
}
}