use super::adjoint::DesignRegion;
pub fn continuation_schedule(n_steps_per_beta: usize, betas: &[f64]) -> Vec<(usize, f64)> {
betas
.iter()
.enumerate()
.map(|(i, &b)| (i * n_steps_per_beta, b))
.collect()
}
pub struct Pseudo2dFom {
pub nx: usize,
pub nz: usize,
}
impl Pseudo2dFom {
pub fn new(nx: usize, nz: usize) -> Self {
Self { nx, nz }
}
pub fn evaluate(&self, projected: &[f64]) -> (f64, Vec<f64>) {
let n = self.nx * self.nz;
let upper_half_start = n / 2;
let n_upper = n - upper_half_start;
let fom = projected[upper_half_start..].iter().sum::<f64>() / n_upper as f64;
let mut grad = vec![0.0_f64; n];
for g in grad.iter_mut().take(n).skip(upper_half_start) {
*g = 1.0 / n_upper as f64;
}
(fom, grad)
}
}
pub struct TopologyOptimizer {
pub region: DesignRegion,
pub filter_radius: f64,
pub beta: f64,
pub eta: f64,
pub iteration: usize,
pub fom_history: Vec<f64>,
}
fn projected_volume(
rho: &[f64],
beta: f64,
eta: f64,
filter_radius: f64,
nx: usize,
nz: usize,
) -> f64 {
let n = nx * nz;
let r = filter_radius;
let r_int = r.ceil() as i64;
let tanh_b_eta = (beta * eta).tanh();
let denom = tanh_b_eta + (beta * (1.0 - eta)).tanh();
let mut total = 0.0_f64;
for j in 0..nz {
for i in 0..nx {
let mut weight_sum = 0.0_f64;
let mut val_sum = 0.0_f64;
for dj in -r_int..=r_int {
for di in -r_int..=r_int {
let dist2 = (di * di + dj * dj) as f64;
if dist2 > r * r {
continue;
}
let ni = i as i64 + di;
let nj = j as i64 + dj;
if ni < 0 || ni >= nx as i64 || nj < 0 || nj >= nz as i64 {
continue;
}
let w = (-(dist2) / (r * r)).exp();
weight_sum += w;
val_sum += w * rho[nj as usize * nx + ni as usize];
}
}
let filtered_ij = val_sum / weight_sum.max(1e-30);
let projected_ij = (tanh_b_eta + (beta * (filtered_ij - eta)).tanh()) / denom;
total += projected_ij;
}
}
total / n as f64
}
impl TopologyOptimizer {
pub fn new(region: DesignRegion, filter_radius: f64) -> Self {
Self {
region,
filter_radius,
beta: 1.0,
eta: 0.5,
iteration: 0,
fom_history: Vec::new(),
}
}
pub fn filter_density(&self) -> Vec<f64> {
let nx = self.region.nx;
let nz = self.region.nz;
let r = self.filter_radius;
let mut filtered = vec![0.0; nx * nz];
for j in 0..nz {
for i in 0..nx {
let mut weight_sum = 0.0;
let mut val_sum = 0.0;
let r_int = r.ceil() as i64;
for dj in -r_int..=r_int {
for di in -r_int..=r_int {
let dist2 = (di * di + dj * dj) as f64;
if dist2 > r * r {
continue;
}
let ni = i as i64 + di;
let nj = j as i64 + dj;
if ni < 0 || ni >= nx as i64 || nj < 0 || nj >= nz as i64 {
continue;
}
let w = (-(dist2) / (r * r)).exp();
weight_sum += w;
val_sum += w * self.region.rho[nj as usize * nx + ni as usize];
}
}
filtered[j * nx + i] = val_sum / weight_sum.max(1e-30);
}
}
filtered
}
pub fn filter_adjoint(&self, du_dfiltered: &[f64]) -> Vec<f64> {
let nx = self.region.nx;
let nz = self.region.nz;
let r = self.filter_radius;
let r_int = r.ceil() as i64;
let mut weight_sums = vec![0.0_f64; nx * nz];
for j in 0..nz {
for i in 0..nx {
let mut ws = 0.0_f64;
for dj in -r_int..=r_int {
for di in -r_int..=r_int {
let dist2 = (di * di + dj * dj) as f64;
if dist2 > r * r {
continue;
}
let ni = i as i64 + di;
let nj = j as i64 + dj;
if ni < 0 || ni >= nx as i64 || nj < 0 || nj >= nz as i64 {
continue;
}
ws += (-(dist2) / (r * r)).exp();
}
}
weight_sums[j * nx + i] = ws.max(1e-30);
}
}
let mut result = vec![0.0_f64; nx * nz];
for j in 0..nz {
for i in 0..nx {
let mut acc = 0.0_f64;
for dj in -r_int..=r_int {
for di in -r_int..=r_int {
let dist2 = (di * di + dj * dj) as f64;
if dist2 > r * r {
continue;
}
let ni = i as i64 + di;
let nj = j as i64 + dj;
if ni < 0 || ni >= nx as i64 || nj < 0 || nj >= nz as i64 {
continue;
}
let w = (-(dist2) / (r * r)).exp();
let src_idx = nj as usize * nx + ni as usize;
acc += w * du_dfiltered[src_idx] / weight_sums[src_idx];
}
}
result[j * nx + i] = acc;
}
}
result
}
pub fn projection_jacobian(&self, filtered: &[f64]) -> Vec<f64> {
let beta = self.beta;
let eta = self.eta;
let denom = (beta * eta).tanh() + (beta * (1.0 - eta)).tanh();
filtered
.iter()
.map(|&rho_tilde| {
let cx = (beta * (rho_tilde - eta)).cosh();
beta / (cx * cx * denom)
})
.collect()
}
pub fn raw_gradient(&self, grad_projected: &[f64]) -> Vec<f64> {
let filtered = self.filter_density();
let proj_jac = self.projection_jacobian(&filtered);
let d_filtered: Vec<f64> = proj_jac
.iter()
.zip(grad_projected.iter())
.map(|(j, g)| j * g)
.collect();
self.filter_adjoint(&d_filtered)
}
fn apply_oc_update(&self, gradient: &[f64], lambda: f64, move_limit: f64) -> Vec<f64> {
const RHO_MIN: f64 = 1e-3;
const RHO_MAX: f64 = 1.0;
self.region
.rho
.iter()
.zip(gradient.iter())
.map(|(&rho_i, &g_i)| {
let be = (g_i / lambda).max(0.0).sqrt();
let rho_candidate = rho_i * be;
let rho_clamped_move = rho_candidate.clamp(rho_i - move_limit, rho_i + move_limit);
rho_clamped_move.clamp(RHO_MIN, RHO_MAX)
})
.collect()
}
fn volume_fraction_for_lambda(&self, gradient: &[f64], lambda: f64, move_limit: f64) -> f64 {
let rho_new = self.apply_oc_update(gradient, lambda, move_limit);
projected_volume(
&rho_new,
self.beta,
self.eta,
self.filter_radius,
self.region.nx,
self.region.nz,
)
}
pub fn oc_step(
&mut self,
gradient: &[f64],
target_volume: f64,
move_limit: f64,
) -> Result<f64, crate::error::OxiPhotonError> {
const BISECT_TOL: f64 = 1e-4;
const MAX_ITER: usize = 50;
let mut lambda_lo = 1e-9_f64;
let mut lambda_hi = 1e9_f64;
let vol_lo = self.volume_fraction_for_lambda(gradient, lambda_lo, move_limit);
let vol_hi = self.volume_fraction_for_lambda(gradient, lambda_hi, move_limit);
if !(vol_hi <= target_volume + BISECT_TOL && vol_lo >= target_volume - BISECT_TOL) {
let rho_new = if (target_volume - vol_hi).abs() < (target_volume - vol_lo).abs() {
self.apply_oc_update(gradient, lambda_hi, move_limit)
} else {
self.apply_oc_update(gradient, lambda_lo, move_limit)
};
let actual_vol = projected_volume(
&rho_new,
self.beta,
self.eta,
self.filter_radius,
self.region.nx,
self.region.nz,
);
self.region.rho = rho_new;
self.iteration += 1;
return Err(crate::error::OxiPhotonError::Convergence(format!(
"OC bisection failed to bracket target volume {target_volume:.4}; \
achievable range [{vol_hi:.4}, {vol_lo:.4}], applied nearest ({actual_vol:.4})"
)));
}
let mut lambda_mid = lambda_lo;
for _ in 0..MAX_ITER {
lambda_mid = (lambda_lo + lambda_hi) / 2.0;
let vol_mid = self.volume_fraction_for_lambda(gradient, lambda_mid, move_limit);
if (vol_mid - target_volume).abs() < BISECT_TOL {
break;
}
if vol_mid > target_volume {
lambda_lo = lambda_mid;
} else {
lambda_hi = lambda_mid;
}
}
let rho_new = self.apply_oc_update(gradient, lambda_mid, move_limit);
let final_vol = projected_volume(
&rho_new,
self.beta,
self.eta,
self.filter_radius,
self.region.nx,
self.region.nz,
);
self.region.rho = rho_new;
self.iteration += 1;
Ok(final_vol)
}
pub fn apply_continuation(&mut self, schedule: &[(usize, f64)]) {
for &(start, beta) in schedule.iter().rev() {
if self.iteration >= start {
self.beta = beta;
break;
}
}
}
pub fn project_density(&self, filtered: &[f64]) -> Vec<f64> {
let beta = self.beta;
let eta = self.eta;
let tanh_b_eta = (beta * eta).tanh();
let denom = tanh_b_eta + (beta * (1.0 - eta)).tanh();
filtered
.iter()
.map(|&rho| (tanh_b_eta + (beta * (rho - eta)).tanh()) / denom)
.collect()
}
pub fn min_feature_size(&self) -> f64 {
2.0 * self.filter_radius * self.region.dx
}
pub fn increase_beta(&mut self, factor: f64) {
self.beta *= factor;
}
pub fn binarisation_fraction(&self, threshold: f64) -> f64 {
let n_binary = self
.region
.rho
.iter()
.filter(|&&r| (r - 0.5).abs() > threshold)
.count();
n_binary as f64 / self.region.n_params() as f64
}
pub fn is_binarised(&self) -> bool {
self.binarisation_fraction(0.45) > 0.95
}
pub fn step(&mut self, dfom_drbar: &[f64], step_size: f64) {
let filtered = self.filter_density();
let dproj = self.projection_jacobian(&filtered);
let dfom_drtilde: Vec<f64> = dfom_drbar
.iter()
.zip(dproj.iter())
.map(|(g, j)| g * j)
.collect();
let dfom_drho_raw = self.filter_adjoint(&dfom_drtilde);
for (rho, &g) in self.region.rho.iter_mut().zip(dfom_drho_raw.iter()) {
*rho = (*rho + step_size * g).clamp(0.0, 1.0);
}
self.iteration += 1;
}
pub fn step_with_raw_gradient(&mut self, raw_gradient: &[f64], step_size: f64) {
for (rho, &g) in self.region.rho.iter_mut().zip(raw_gradient.iter()) {
*rho = (*rho + step_size * g).clamp(0.0, 1.0);
}
self.iteration += 1;
}
pub fn record_fom(&mut self, fom: f64) {
self.fom_history.push(fom);
}
pub fn best_fom(&self) -> f64 {
self.fom_history
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
}
pub fn is_converged(&self, n_window: usize, tolerance: f64) -> bool {
if self.fom_history.len() < n_window {
return false;
}
let n = self.fom_history.len();
let recent: &[f64] = &self.fom_history[n - n_window..];
let max = recent.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min = recent.iter().cloned().fold(f64::INFINITY, f64::min);
(max - min).abs() < tolerance
}
}
pub struct BinaryProjection;
impl BinaryProjection {
pub fn hard_threshold(rho: &[f64]) -> Vec<f64> {
rho.iter()
.map(|&r| if r > 0.5 { 1.0 } else { 0.0 })
.collect()
}
pub fn erode(binary: &[f64], nx: usize, nz: usize) -> Vec<f64> {
let mut result = binary.to_vec();
for j in 0..nz {
for i in 0..nx {
if binary[j * nx + i] < 0.5 {
continue; }
let is_edge = (i == 0)
|| (i == nx - 1)
|| (j == 0)
|| (j == nz - 1)
|| (i > 0 && binary[j * nx + (i - 1)] < 0.5)
|| (i < nx - 1 && binary[j * nx + (i + 1)] < 0.5)
|| (j > 0 && binary[(j - 1) * nx + i] < 0.5)
|| (j < nz - 1 && binary[(j + 1) * nx + i] < 0.5);
if is_edge {
result[j * nx + i] = 0.0;
}
}
}
result
}
pub fn dilate(binary: &[f64], nx: usize, nz: usize) -> Vec<f64> {
let mut result = binary.to_vec();
for j in 0..nz {
for i in 0..nx {
if binary[j * nx + i] > 0.5 {
continue; }
let has_neighbor = (i > 0 && binary[j * nx + (i - 1)] > 0.5)
|| (i < nx - 1 && binary[j * nx + (i + 1)] > 0.5)
|| (j > 0 && binary[(j - 1) * nx + i] > 0.5)
|| (j < nz - 1 && binary[(j + 1) * nx + i] > 0.5);
if has_neighbor {
result[j * nx + i] = 1.0;
}
}
}
result
}
pub fn opening(binary: &[f64], nx: usize, nz: usize) -> Vec<f64> {
let eroded = Self::erode(binary, nx, nz);
Self::dilate(&eroded, nx, nz)
}
pub fn closing(binary: &[f64], nx: usize, nz: usize) -> Vec<f64> {
let dilated = Self::dilate(binary, nx, nz);
Self::erode(&dilated, nx, nz)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_preserves_uniform_design() {
let region = DesignRegion::new(8, 8, 20e-9, 1.0, 4.0);
let opt = TopologyOptimizer::new(region, 2.0);
let filtered = opt.filter_density();
for &f in &filtered {
assert!((f - 0.5).abs() < 1e-6, "f={f:.4}");
}
}
#[test]
fn projection_sharpens_near_boundary() {
let region = DesignRegion::new(4, 1, 20e-9, 1.0, 4.0);
let mut opt = TopologyOptimizer::new(region, 1.0);
opt.beta = 10.0;
let filtered = vec![0.1, 0.4, 0.6, 0.9];
let projected = opt.project_density(&filtered);
assert!(projected[0] < 0.2);
assert!(projected[3] > 0.8);
}
#[test]
fn min_feature_size_physical() {
let region = DesignRegion::new(10, 10, 20e-9, 1.0, 4.0);
let opt = TopologyOptimizer::new(region, 2.0);
let mfs = opt.min_feature_size();
assert!((mfs - 80e-9).abs() < 1e-12);
}
#[test]
fn binary_hard_threshold() {
let rho = vec![0.2, 0.5, 0.7, 0.1, 0.9];
let binary = BinaryProjection::hard_threshold(&rho);
assert_eq!(binary[0], 0.0);
assert_eq!(binary[2], 1.0);
assert_eq!(binary[4], 1.0);
}
#[test]
fn erode_reduces_fill() {
let nx = 5;
let nz = 5;
let binary = vec![1.0; nx * nz]; let eroded = BinaryProjection::erode(&binary, nx, nz);
let fill_before = binary.iter().sum::<f64>() / (nx * nz) as f64;
let fill_after = eroded.iter().sum::<f64>() / (nx * nz) as f64;
assert!(fill_after < fill_before);
}
#[test]
fn dilate_increases_fill() {
let nx = 5;
let nz = 5;
let mut binary = vec![0.0; nx * nz];
binary[2 * nx + 2] = 1.0; let dilated = BinaryProjection::dilate(&binary, nx, nz);
let fill = dilated.iter().sum::<f64>();
assert!(fill > 1.0); }
#[test]
fn convergence_detection() {
let region = DesignRegion::new(4, 4, 20e-9, 1.0, 4.0);
let mut opt = TopologyOptimizer::new(region, 1.0);
for _ in 0..10 {
opt.record_fom(1.0 + 1e-10); }
assert!(opt.is_converged(5, 1e-6));
}
#[test]
fn binarisation_fraction_initial() {
let region = DesignRegion::new(10, 10, 20e-9, 1.0, 4.0);
let opt = TopologyOptimizer::new(region, 1.0);
assert_eq!(opt.binarisation_fraction(0.45), 0.0);
}
}