use std::f64::consts::PI;
const T_AMBIENT: f64 = 298.15;
#[inline]
pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
pub fn norm3(v: [f64; 3]) -> f64 {
dot3(v, v).sqrt()
}
#[inline]
pub fn normalise3(v: [f64; 3]) -> [f64; 3] {
let n = norm3(v);
if n < 1.0e-300 {
[0.0; 3]
} else {
[v[0] / n, v[1] / n, v[2] / n]
}
}
#[inline]
pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
pub fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ElementState {
Inactive,
JustActivated,
Active,
}
#[derive(Debug, Clone)]
pub struct LayerActivation {
pub n_elements: usize,
pub element_layer: Vec<usize>,
pub element_state: Vec<ElementState>,
pub current_layer: usize,
pub n_layers: usize,
pub layer_thickness: f64,
pub time_per_layer: f64,
}
impl LayerActivation {
pub fn new(
n_elements: usize,
element_layer: Vec<usize>,
n_layers: usize,
layer_thickness: f64,
time_per_layer: f64,
) -> Self {
assert_eq!(element_layer.len(), n_elements);
let element_state = vec![ElementState::Inactive; n_elements];
Self {
n_elements,
element_layer,
element_state,
current_layer: 0,
n_layers,
layer_thickness,
time_per_layer,
}
}
pub fn advance_layer(&mut self) -> Vec<usize> {
let mut newly_activated = Vec::new();
for s in self.element_state.iter_mut() {
if *s == ElementState::JustActivated {
*s = ElementState::Active;
}
}
for (idx, &layer) in self.element_layer.iter().enumerate() {
if layer == self.current_layer && self.element_state[idx] == ElementState::Inactive {
self.element_state[idx] = ElementState::JustActivated;
newly_activated.push(idx);
}
}
if self.current_layer < self.n_layers.saturating_sub(1) {
self.current_layer += 1;
}
newly_activated
}
pub fn active_fraction(&self) -> f64 {
let active = self
.element_state
.iter()
.filter(|&&s| s == ElementState::Active || s == ElementState::JustActivated)
.count();
active as f64 / self.n_elements as f64
}
pub fn build_height(&self) -> f64 {
self.current_layer as f64 * self.layer_thickness
}
pub fn elapsed_time(&self) -> f64 {
self.current_layer as f64 * self.time_per_layer
}
pub fn is_active(&self, element_idx: usize) -> bool {
matches!(
self.element_state[element_idx],
ElementState::Active | ElementState::JustActivated
)
}
}
#[derive(Debug, Clone)]
pub struct GoldakSource {
pub power: f64,
pub efficiency: f64,
pub a: f64,
pub b: f64,
pub c_front: f64,
pub c_rear: f64,
pub f_front: f64,
pub f_rear: f64,
}
impl GoldakSource {
pub fn new(power: f64, efficiency: f64, a: f64, b: f64, c_front: f64, c_rear: f64) -> Self {
Self {
power,
efficiency,
a,
b,
c_front,
c_rear,
f_front: 0.6,
f_rear: 1.4,
}
}
pub fn heat_flux(&self, p: [f64; 3], source_pos: [f64; 3]) -> f64 {
let effective_power = self.power * self.efficiency;
let dx = p[0] - source_pos[0];
let dy = p[1] - source_pos[1];
let dz = p[2] - source_pos[2];
let (c, f) = if dx >= 0.0 {
(self.c_front, self.f_front)
} else {
(self.c_rear, self.f_rear)
};
let prefactor =
6.0 * 3.0_f64.sqrt() * f * effective_power / (PI * PI.sqrt() * self.a * self.b * c);
let exponent =
-3.0 * (dx * dx / (self.a * self.a) + dy * dy / (self.b * self.b) + dz * dz / (c * c));
prefactor * exponent.exp()
}
}
#[derive(Debug, Clone)]
pub struct ThermalHistoryFem {
pub n_nodes: usize,
pub positions: Vec<[f64; 3]>,
pub temperatures: Vec<f64>,
pub peak_temperatures: Vec<f64>,
pub source: GoldakSource,
pub t_liquidus: f64,
pub t_solidus: f64,
pub conductivity: f64,
pub specific_heat: f64,
pub density: f64,
}
impl ThermalHistoryFem {
pub fn new(
positions: Vec<[f64; 3]>,
source: GoldakSource,
t_liquidus: f64,
t_solidus: f64,
conductivity: f64,
specific_heat: f64,
density: f64,
) -> Self {
let n = positions.len();
Self {
n_nodes: n,
positions,
temperatures: vec![T_AMBIENT; n],
peak_temperatures: vec![T_AMBIENT; n],
source,
t_liquidus,
t_solidus,
conductivity,
specific_heat,
density,
}
}
pub fn step(&mut self, source_pos: [f64; 3], dt: f64, h_conv: f64, node_volume: f64) {
let rho_cp = self.density * self.specific_heat;
for i in 0..self.n_nodes {
let q = self.source.heat_flux(self.positions[i], source_pos);
let t = self.temperatures[i];
let dt_node = (q / rho_cp - h_conv * (t - T_AMBIENT) / rho_cp / node_volume) * dt;
self.temperatures[i] = (t + dt_node).max(T_AMBIENT);
if self.temperatures[i] > self.peak_temperatures[i] {
self.peak_temperatures[i] = self.temperatures[i];
}
}
}
pub fn melt_pool_nodes(&self) -> Vec<usize> {
self.temperatures
.iter()
.enumerate()
.filter(|&(_, &t)| t >= self.t_liquidus)
.map(|(i, _)| i)
.collect()
}
pub fn mushy_zone_fraction(&self) -> f64 {
let mushy = self
.temperatures
.iter()
.filter(|&&t| t >= self.t_solidus)
.count();
mushy as f64 / self.n_nodes as f64
}
pub fn thermal_diffusivity(&self) -> f64 {
self.conductivity / (self.density * self.specific_heat)
}
pub fn temperature_dependent_conductivity(&self, temperature: f64, beta: f64) -> f64 {
let t_ref = 298.15;
self.conductivity * (1.0 + beta * (temperature - t_ref))
}
}
#[derive(Debug, Clone)]
pub struct StressState {
pub stress: [f64; 6],
pub plastic_strain: [f64; 6],
pub eq_plastic_strain: f64,
}
impl StressState {
pub fn zero() -> Self {
Self {
stress: [0.0; 6],
plastic_strain: [0.0; 6],
eq_plastic_strain: 0.0,
}
}
pub fn von_mises(&self) -> f64 {
let s = &self.stress;
let dev_xx = s[0] - (s[0] + s[1] + s[2]) / 3.0;
let dev_yy = s[1] - (s[0] + s[1] + s[2]) / 3.0;
let dev_zz = s[2] - (s[0] + s[1] + s[2]) / 3.0;
(0.5 * (dev_xx * dev_xx
+ dev_yy * dev_yy
+ dev_zz * dev_zz
+ 2.0 * (s[3] * s[3] + s[4] * s[4] + s[5] * s[5])))
.sqrt()
* 3.0_f64.sqrt()
}
pub fn pressure(&self) -> f64 {
(self.stress[0] + self.stress[1] + self.stress[2]) / 3.0
}
}
#[derive(Debug, Clone)]
pub struct ResidualStressFem {
pub n_points: usize,
pub states: Vec<StressState>,
pub young_modulus: f64,
pub poisson_ratio: f64,
pub yield_stress_ref: f64,
pub thermal_expansion: f64,
pub hardening_modulus: f64,
}
impl ResidualStressFem {
pub fn new(
n_points: usize,
young_modulus: f64,
poisson_ratio: f64,
yield_stress_ref: f64,
thermal_expansion: f64,
hardening_modulus: f64,
) -> Self {
Self {
n_points,
states: vec![StressState::zero(); n_points],
young_modulus,
poisson_ratio,
yield_stress_ref,
thermal_expansion,
hardening_modulus,
}
}
pub fn lame_lambda(&self) -> f64 {
self.young_modulus * self.poisson_ratio
/ ((1.0 + self.poisson_ratio) * (1.0 - 2.0 * self.poisson_ratio))
}
pub fn shear_modulus(&self) -> f64 {
self.young_modulus / (2.0 * (1.0 + self.poisson_ratio))
}
pub fn yield_stress_at_temperature(&self, temperature: f64, t_melt: f64) -> f64 {
let t_ref = T_AMBIENT;
let factor = 1.0 - (temperature - t_ref) / (t_melt - t_ref);
self.yield_stress_ref * factor.max(0.0)
}
pub fn radial_return(
&mut self,
point_idx: usize,
d_eps_th: f64,
temperature: f64,
t_melt: f64,
) -> f64 {
let g = self.shear_modulus();
let lam = self.lame_lambda();
let sigma_y = self.yield_stress_at_temperature(temperature, t_melt)
+ self.hardening_modulus * self.states[point_idx].eq_plastic_strain;
let d_sig_th = (3.0 * lam + 2.0 * g) * self.thermal_expansion * d_eps_th;
self.states[point_idx].stress[0] -= d_sig_th;
self.states[point_idx].stress[1] -= d_sig_th;
self.states[point_idx].stress[2] -= d_sig_th;
let vm_trial = self.states[point_idx].von_mises();
if vm_trial <= sigma_y {
return 0.0;
}
let d_gamma = (vm_trial - sigma_y) / (3.0 * g + self.hardening_modulus);
let scale = 1.0 - 3.0 * g * d_gamma / vm_trial;
let p = self.states[point_idx].pressure();
for k in 0..6 {
let dev = if k < 3 {
self.states[point_idx].stress[k] - p
} else {
self.states[point_idx].stress[k]
};
let corrected = dev * scale;
if k < 3 {
self.states[point_idx].stress[k] = corrected + p;
} else {
self.states[point_idx].stress[k] = corrected;
}
}
self.states[point_idx].eq_plastic_strain += d_gamma;
d_gamma
}
pub fn max_von_mises(&self) -> f64 {
self.states
.iter()
.map(|s| s.von_mises())
.fold(0.0_f64, f64::max)
}
pub fn mean_residual_pressure(&self) -> f64 {
let sum: f64 = self.states.iter().map(|s| s.pressure()).sum();
sum / self.n_points as f64
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScanStrategy {
Unidirectional,
Bidirectional,
Island,
Spiral,
Rotating90,
}
#[derive(Debug, Clone)]
pub struct PowderBedFusion {
pub power: f64,
pub scan_speed: f64,
pub hatch_spacing: f64,
pub layer_thickness: f64,
pub absorptivity: f64,
pub powder_density: f64,
pub melting_point: f64,
pub scan_strategy: ScanStrategy,
}
impl PowderBedFusion {
pub fn new(
power: f64,
scan_speed: f64,
hatch_spacing: f64,
layer_thickness: f64,
absorptivity: f64,
powder_density: f64,
melting_point: f64,
scan_strategy: ScanStrategy,
) -> Self {
Self {
power,
scan_speed,
hatch_spacing,
layer_thickness,
absorptivity,
powder_density,
melting_point,
scan_strategy,
}
}
pub fn energy_density(&self) -> f64 {
self.power / (self.scan_speed * self.hatch_spacing * self.layer_thickness)
}
pub fn linear_energy_density(&self) -> f64 {
self.power / self.scan_speed
}
pub fn is_above_melting_threshold(&self, specific_heat: f64, efficiency: f64) -> bool {
let e_th = self.powder_density * specific_heat * (self.melting_point - T_AMBIENT) * PI
/ (4.0 * efficiency);
self.energy_density() > e_th
}
pub fn melt_pool_depth(&self, conductivity: f64, efficiency: f64) -> f64 {
let numerator = 2.0 * efficiency * self.power;
let denominator = PI
* std::f64::consts::E
* conductivity
* self.scan_speed
* (self.melting_point - T_AMBIENT);
(numerator / denominator).sqrt()
}
pub fn tracks_per_layer(&self, part_width: f64) -> usize {
(part_width / self.hatch_spacing).ceil() as usize
}
pub fn total_scan_length_per_layer(&self, width: f64, depth: f64) -> f64 {
let n_tracks = self.tracks_per_layer(width);
n_tracks as f64 * depth
}
}
#[derive(Debug, Clone)]
pub struct DirectedEnergyDeposition {
pub power: f64,
pub travel_speed: f64,
pub powder_feed_rate: f64,
pub catchment_efficiency: f64,
pub substrate_absorptivity: f64,
pub clad_height: f64,
pub clad_width: f64,
}
impl DirectedEnergyDeposition {
pub fn new(
power: f64,
travel_speed: f64,
powder_feed_rate: f64,
catchment_efficiency: f64,
substrate_absorptivity: f64,
clad_height: f64,
clad_width: f64,
) -> Self {
Self {
power,
travel_speed,
powder_feed_rate,
catchment_efficiency,
substrate_absorptivity,
clad_height,
clad_width,
}
}
pub fn effective_power(&self) -> f64 {
self.power * self.substrate_absorptivity
}
pub fn deposition_rate(&self) -> f64 {
self.powder_feed_rate * self.catchment_efficiency
}
pub fn dilution_ratio(&self, density: f64, fusion_enthalpy: f64) -> f64 {
let ep = self.effective_power();
let a_clad = self.clad_height * self.clad_width;
let denom = ep + self.travel_speed * a_clad * density * fusion_enthalpy;
ep / denom
}
pub fn clad_area(&self) -> f64 {
PI * self.clad_height * self.clad_width / 4.0
}
pub fn bonding_factor(&self, density: f64, fusion_enthalpy: f64) -> f64 {
let d = self.dilution_ratio(density, fusion_enthalpy);
if d < 0.05 {
d / 0.05
} else if d <= 0.35 {
1.0
} else {
(1.0 - (d - 0.35) / 0.65).max(0.0)
}
}
}
#[derive(Debug, Clone)]
pub struct SupportStructure {
pub critical_angle: f64,
pub material_fraction: f64,
pub stiffness_scale: f64,
pub n_support_elements: usize,
pub support_stress: Vec<f64>,
}
impl SupportStructure {
pub fn new(
critical_angle_deg: f64,
material_fraction: f64,
stiffness_scale: f64,
n_support_elements: usize,
) -> Self {
Self {
critical_angle: critical_angle_deg.to_radians(),
material_fraction,
stiffness_scale,
n_support_elements,
support_stress: vec![0.0; n_support_elements],
}
}
pub fn needs_support(&self, facet_normal: [f64; 3]) -> bool {
let build_dir = [0.0_f64, 0.0, 1.0];
let cos_angle = dot3(facet_normal, build_dir).abs();
let angle = cos_angle.acos();
angle < self.critical_angle
}
pub fn required_volume_fraction(&self, overhang_angle_rad: f64) -> f64 {
let ratio = overhang_angle_rad / self.critical_angle;
(1.0 - ratio).max(0.0) * self.material_fraction
}
pub fn removal_stress(&self, element_idx: usize, part_weight: f64, support_area: f64) -> f64 {
let base_stress = part_weight / support_area;
self.support_stress[element_idx] + base_stress * self.stiffness_scale
}
pub fn max_support_stress(&self) -> f64 {
self.support_stress.iter().cloned().fold(0.0_f64, f64::max)
}
}
#[derive(Debug, Clone)]
pub struct JmakTransformation {
pub n_avrami: f64,
pub k_rate: f64,
}
impl JmakTransformation {
pub fn new(n_avrami: f64, k_rate: f64) -> Self {
Self { n_avrami, k_rate }
}
pub fn transformed_fraction(&self, time: f64) -> f64 {
let exponent = -self.k_rate * time.powf(self.n_avrami);
1.0 - exponent.exp()
}
pub fn incubation_time(&self, x0: f64) -> f64 {
let ln_term = -(1.0 - x0).ln();
(ln_term / self.k_rate).powf(1.0 / self.n_avrami)
}
}
#[derive(Debug, Clone)]
pub struct HillertGrainGrowth {
pub mobility_prefactor: f64,
pub boundary_energy: f64,
pub activation_energy: f64,
pub gas_constant: f64,
}
impl HillertGrainGrowth {
pub fn new(mobility_prefactor: f64, boundary_energy: f64, activation_energy: f64) -> Self {
Self {
mobility_prefactor,
boundary_energy,
activation_energy,
gas_constant: 8.314,
}
}
pub fn mobility(&self, temperature: f64) -> f64 {
self.mobility_prefactor
* (-self.activation_energy / (self.gas_constant * temperature)).exp()
}
pub fn grain_radius_after(&self, r0: f64, dt: f64, temperature: f64) -> f64 {
let m = self.mobility(temperature);
let r_sq = r0 * r0 + 2.0 * m * self.boundary_energy * dt;
r_sq.sqrt()
}
pub fn critical_radius(&self, mean_radius: f64) -> f64 {
2.0 * mean_radius
}
}
#[derive(Debug, Clone)]
pub struct MicrostructureEvolution {
pub grain_radius: f64,
pub transformed_fraction: f64,
pub grain_growth: HillertGrainGrowth,
pub jmak: JmakTransformation,
pub texture_index: f64,
}
impl MicrostructureEvolution {
pub fn new(
initial_grain_radius: f64,
grain_growth: HillertGrainGrowth,
jmak: JmakTransformation,
) -> Self {
Self {
grain_radius: initial_grain_radius,
transformed_fraction: 0.0,
grain_growth,
jmak,
texture_index: 1.0,
}
}
pub fn step(&mut self, dt: f64, temperature: f64, cumulative_time: f64) {
self.grain_radius =
self.grain_growth
.grain_radius_after(self.grain_radius, dt, temperature);
self.transformed_fraction = self.jmak.transformed_fraction(cumulative_time + dt);
self.texture_index = 1.0 + self.transformed_fraction * 2.5;
}
pub fn hall_petch_strength(&self, sigma0: f64, k_hp: f64) -> f64 {
let diameter = 2.0 * self.grain_radius;
sigma0 + k_hp / diameter.sqrt()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PorosityType {
LackOfFusion,
Keyhole,
Gas,
}
#[derive(Debug, Clone)]
pub struct PorosityModel {
pub porosity: f64,
pub lof_fraction: f64,
pub keyhole_fraction: f64,
pub gas_fraction: f64,
pub dense_young_modulus: f64,
pub dense_yield_stress: f64,
}
impl PorosityModel {
pub fn new(dense_young_modulus: f64, dense_yield_stress: f64) -> Self {
Self {
porosity: 0.0,
lof_fraction: 0.0,
keyhole_fraction: 0.0,
gas_fraction: 0.0,
dense_young_modulus,
dense_yield_stress,
}
}
pub fn update_from_energy_density(
&mut self,
energy_density: f64,
e_low: f64,
e_high: f64,
gas_base: f64,
) {
self.gas_fraction = gas_base;
if energy_density < e_low {
let deficit = (e_low - energy_density) / e_low;
self.lof_fraction = 0.15 * deficit * deficit;
self.keyhole_fraction = 0.0;
} else if energy_density > e_high {
let excess = (energy_density - e_high) / e_high;
self.keyhole_fraction = 0.08 * excess;
self.lof_fraction = 0.0;
} else {
self.lof_fraction = 0.0;
self.keyhole_fraction = 0.0;
}
self.porosity = self.lof_fraction + self.keyhole_fraction + self.gas_fraction;
}
pub fn effective_young_modulus(&self) -> f64 {
self.dense_young_modulus * (1.0 - self.porosity).powf(2.0)
}
pub fn effective_yield_stress(&self) -> f64 {
self.dense_yield_stress * (1.0 - self.porosity).powf(1.5)
}
pub fn relative_density(&self) -> f64 {
1.0 - self.porosity
}
}
#[derive(Debug, Clone)]
pub struct DistortionCompensation {
pub n_nodes: usize,
pub nominal_positions: Vec<[f64; 3]>,
pub distorted_positions: Vec<[f64; 3]>,
pub compensated_positions: Vec<[f64; 3]>,
pub scaling_factors: [f64; 3],
pub compensation_fraction: f64,
}
impl DistortionCompensation {
pub fn new(nominal_positions: Vec<[f64; 3]>, compensation_fraction: f64) -> Self {
let n = nominal_positions.len();
let distorted_positions = nominal_positions.clone();
let compensated_positions = nominal_positions.clone();
Self {
n_nodes: n,
nominal_positions,
distorted_positions,
compensated_positions,
scaling_factors: [1.0; 3],
compensation_fraction,
}
}
pub fn set_distorted_positions(&mut self, positions: Vec<[f64; 3]>) {
assert_eq!(positions.len(), self.n_nodes);
self.distorted_positions = positions;
self.update_compensated();
}
fn update_compensated(&mut self) {
for i in 0..self.n_nodes {
let nom = self.nominal_positions[i];
let dist = self.distorted_positions[i];
let delta = sub3(dist, nom);
let correction = scale3(delta, -self.compensation_fraction);
self.compensated_positions[i] = add3(nom, correction);
}
}
pub fn apply_scaling(&mut self) {
let sf = self.scaling_factors;
for pos in self.compensated_positions.iter_mut() {
pos[0] *= sf[0];
pos[1] *= sf[1];
pos[2] *= sf[2];
}
}
pub fn rms_distortion(&self) -> f64 {
let sum_sq: f64 = self
.distorted_positions
.iter()
.zip(self.nominal_positions.iter())
.map(|(&d, &n)| {
let delta = sub3(d, n);
dot3(delta, delta)
})
.sum();
(sum_sq / self.n_nodes as f64).sqrt()
}
pub fn max_distortion(&self) -> f64 {
self.distorted_positions
.iter()
.zip(self.nominal_positions.iter())
.map(|(&d, &n)| norm3(sub3(d, n)))
.fold(0.0_f64, f64::max)
}
}
#[derive(Debug, Clone)]
pub struct OptimizationWeights {
pub stress_weight: f64,
pub distortion_weight: f64,
pub time_weight: f64,
pub porosity_weight: f64,
}
impl OptimizationWeights {
pub fn uniform() -> Self {
Self {
stress_weight: 1.0,
distortion_weight: 1.0,
time_weight: 1.0,
porosity_weight: 1.0,
}
}
pub fn normalise(&self) -> Self {
let total =
self.stress_weight + self.distortion_weight + self.time_weight + self.porosity_weight;
Self {
stress_weight: self.stress_weight / total,
distortion_weight: self.distortion_weight / total,
time_weight: self.time_weight / total,
porosity_weight: self.porosity_weight / total,
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessParameters {
pub power: f64,
pub scan_speed: f64,
pub hatch_spacing: f64,
pub layer_thickness: f64,
}
impl ProcessParameters {
pub fn new(power: f64, scan_speed: f64, hatch_spacing: f64, layer_thickness: f64) -> Self {
Self {
power,
scan_speed,
hatch_spacing,
layer_thickness,
}
}
pub fn energy_density(&self) -> f64 {
self.power / (self.scan_speed * self.hatch_spacing * self.layer_thickness)
}
}
#[derive(Debug, Clone)]
pub struct BuildProcessOptimization {
pub weights: OptimizationWeights,
pub candidates: Vec<ProcessParameters>,
pub objective_values: Vec<f64>,
}
impl BuildProcessOptimization {
pub fn new(weights: OptimizationWeights, candidates: Vec<ProcessParameters>) -> Self {
let n = candidates.len();
Self {
weights,
candidates,
objective_values: vec![f64::INFINITY; n],
}
}
pub fn evaluate_candidate(&mut self, idx: usize, e_opt: f64) {
let p = &self.candidates[idx];
let ev = p.energy_density();
let w = self.weights.normalise();
let stress_term = (ev / e_opt - 1.0).abs();
let distortion_term = (ev / e_opt).sqrt();
let time_term = 1.0 / (p.scan_speed * p.hatch_spacing * p.layer_thickness) * 1.0e-9;
let porosity_term = if (ev - e_opt).abs() < 0.2 * e_opt {
0.0
} else {
(ev - e_opt).abs() / e_opt
};
self.objective_values[idx] = w.stress_weight * stress_term
+ w.distortion_weight * distortion_term
+ w.time_weight * time_term
+ w.porosity_weight * porosity_term;
}
pub fn optimise(&mut self, e_opt: f64) -> usize {
for i in 0..self.candidates.len() {
self.evaluate_candidate(i, e_opt);
}
self.objective_values
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0)
}
pub fn best_parameters(&self) -> Option<&ProcessParameters> {
let best_idx = self
.objective_values
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)?;
Some(&self.candidates[best_idx])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layer_activation_initial_state() {
let layers = vec![0, 0, 1, 1, 2, 2];
let la = LayerActivation::new(6, layers, 3, 5.0e-5, 10.0);
assert_eq!(la.n_elements, 6);
assert!(
la.element_state
.iter()
.all(|&s| s == ElementState::Inactive)
);
}
#[test]
fn test_layer_activation_advance_layer_zero() {
let layers = vec![0, 0, 1, 1, 2, 2];
let mut la = LayerActivation::new(6, layers, 3, 5.0e-5, 10.0);
let activated = la.advance_layer();
assert_eq!(activated.len(), 2);
assert!(activated.contains(&0));
assert!(activated.contains(&1));
}
#[test]
fn test_layer_activation_active_fraction_increases() {
let layers = vec![0, 1, 2, 3];
let mut la = LayerActivation::new(4, layers, 4, 1.0e-4, 5.0);
la.advance_layer();
let f1 = la.active_fraction();
la.advance_layer();
let f2 = la.active_fraction();
assert!(f2 >= f1);
}
#[test]
fn test_layer_activation_build_height() {
let layers = vec![0, 1, 2];
let mut la = LayerActivation::new(3, layers, 3, 1.0e-4, 5.0);
la.advance_layer();
la.advance_layer();
let h = la.build_height();
assert!((h - 2.0e-4).abs() < 1.0e-15, "h = {:.6e}", h);
}
#[test]
fn test_layer_activation_elapsed_time() {
let layers = vec![0, 1];
let mut la = LayerActivation::new(2, layers, 2, 1.0e-4, 5.0);
la.advance_layer();
assert!((la.elapsed_time() - 5.0).abs() < 1.0e-12);
}
#[test]
fn test_layer_activation_is_active() {
let layers = vec![0, 1];
let mut la = LayerActivation::new(2, layers, 2, 1.0e-4, 5.0);
la.advance_layer();
assert!(la.is_active(0));
assert!(!la.is_active(1));
}
#[test]
fn test_goldak_source_zero_at_distance() {
let src = GoldakSource::new(200.0, 0.35, 1.5e-4, 1.0e-4, 1.0e-4, 2.0e-4);
let q = src.heat_flux([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
assert!(q < 1.0, "Heat flux far from source should be negligible");
}
#[test]
fn test_goldak_source_max_at_source_pos() {
let src = GoldakSource::new(200.0, 0.35, 1.5e-4, 1.0e-4, 1.0e-4, 2.0e-4);
let q_center = src.heat_flux([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
let q_away = src.heat_flux([5.0e-4, 0.0, 0.0], [0.0, 0.0, 0.0]);
assert!(q_center > q_away);
}
#[test]
fn test_thermal_history_fem_diffusivity() {
let positions = vec![[0.0; 3]; 4];
let src = GoldakSource::new(200.0, 0.35, 1.5e-4, 1.0e-4, 1.0e-4, 2.0e-4);
let thf = ThermalHistoryFem::new(positions, src, 1923.0, 1723.0, 12.0, 620.0, 8000.0);
let alpha = thf.thermal_diffusivity();
let expected = 12.0 / (8000.0 * 620.0);
assert!((alpha - expected).abs() < 1.0e-20);
}
#[test]
fn test_thermal_history_fem_initial_temperatures() {
let positions = vec![[0.0; 3], [0.01, 0.0, 0.0]];
let src = GoldakSource::new(200.0, 0.35, 1.5e-4, 1.0e-4, 1.0e-4, 2.0e-4);
let thf = ThermalHistoryFem::new(positions, src, 1923.0, 1723.0, 12.0, 620.0, 8000.0);
assert!((thf.temperatures[0] - T_AMBIENT).abs() < 1.0e-10);
}
#[test]
fn test_thermal_history_fem_melt_pool_nodes_empty() {
let positions = vec![[0.0; 3]; 10];
let src = GoldakSource::new(1.0, 0.01, 1.5e-4, 1.0e-4, 1.0e-4, 2.0e-4);
let thf = ThermalHistoryFem::new(positions, src, 1923.0, 1723.0, 12.0, 620.0, 8000.0);
assert!(thf.melt_pool_nodes().is_empty());
}
#[test]
fn test_thermal_history_fem_step_increases_temperature() {
let positions = vec![[0.0; 3]];
let src = GoldakSource::new(2000.0, 0.8, 1.5e-4, 1.0e-4, 1.0e-4, 2.0e-4);
let mut thf = ThermalHistoryFem::new(positions, src, 1923.0, 1723.0, 12.0, 620.0, 8000.0);
let t_init = thf.temperatures[0];
thf.step([0.0; 3], 1.0e-4, 10.0, 1.0e-8);
assert!(thf.temperatures[0] > t_init);
}
#[test]
fn test_residual_stress_lame_lambda() {
let fem = ResidualStressFem::new(4, 200.0e9, 0.3, 450.0e6, 11.7e-6, 5.0e9);
let lam = fem.lame_lambda();
assert!(lam > 0.0);
}
#[test]
fn test_residual_stress_shear_modulus() {
let fem = ResidualStressFem::new(4, 200.0e9, 0.3, 450.0e6, 11.7e-6, 5.0e9);
let g = fem.shear_modulus();
let expected = 200.0e9 / (2.0 * 1.3);
assert!((g - expected).abs() < 1.0);
}
#[test]
fn test_residual_stress_yield_at_ambient() {
let fem = ResidualStressFem::new(4, 200.0e9, 0.3, 450.0e6, 11.7e-6, 5.0e9);
let sy = fem.yield_stress_at_temperature(T_AMBIENT, 1800.0);
assert!((sy - 450.0e6).abs() < 1.0);
}
#[test]
fn test_residual_stress_yield_at_melt() {
let fem = ResidualStressFem::new(4, 200.0e9, 0.3, 450.0e6, 11.7e-6, 5.0e9);
let sy = fem.yield_stress_at_temperature(1800.0, 1800.0);
assert!(
sy < 1.0,
"yield stress should be near zero at melt: {:.4e}",
sy
);
}
#[test]
fn test_von_mises_uniaxial() {
let mut s = StressState::zero();
s.stress[0] = 100.0e6;
let vm = s.von_mises();
assert!((vm - 100.0e6).abs() < 1.0);
}
#[test]
fn test_von_mises_zero() {
let s = StressState::zero();
assert!(s.von_mises() < 1.0e-6);
}
#[test]
fn test_pbf_energy_density() {
let pbf = PowderBedFusion::new(
200.0,
0.8,
1.0e-4,
5.0e-5,
0.35,
4000.0,
1923.0,
ScanStrategy::Bidirectional,
);
let ev = pbf.energy_density();
let expected = 200.0 / (0.8 * 1.0e-4 * 5.0e-5);
assert!((ev - expected).abs() < 1.0);
}
#[test]
fn test_pbf_linear_energy_density() {
let pbf = PowderBedFusion::new(
200.0,
0.8,
1.0e-4,
5.0e-5,
0.35,
4000.0,
1923.0,
ScanStrategy::Unidirectional,
);
assert!((pbf.linear_energy_density() - 250.0).abs() < 1.0e-10);
}
#[test]
fn test_pbf_tracks_per_layer() {
let pbf = PowderBedFusion::new(
200.0,
0.8,
1.0e-4,
5.0e-5,
0.35,
4000.0,
1923.0,
ScanStrategy::Island,
);
let tracks = pbf.tracks_per_layer(1.0e-3);
assert_eq!(tracks, 10);
}
#[test]
fn test_pbf_melt_pool_depth_positive() {
let pbf = PowderBedFusion::new(
200.0,
0.8,
1.0e-4,
5.0e-5,
0.35,
4000.0,
1923.0,
ScanStrategy::Bidirectional,
);
let depth = pbf.melt_pool_depth(12.0, 0.35);
assert!(depth > 0.0);
}
#[test]
fn test_ded_effective_power() {
let ded = DirectedEnergyDeposition::new(2000.0, 0.01, 5.0e-4, 0.7, 0.5, 1.0e-3, 3.0e-3);
assert!((ded.effective_power() - 1000.0).abs() < 1.0e-10);
}
#[test]
fn test_ded_deposition_rate() {
let ded = DirectedEnergyDeposition::new(2000.0, 0.01, 5.0e-4, 0.7, 0.5, 1.0e-3, 3.0e-3);
assert!((ded.deposition_rate() - 3.5e-4).abs() < 1.0e-15);
}
#[test]
fn test_ded_dilution_ratio_bounds() {
let ded = DirectedEnergyDeposition::new(2000.0, 0.01, 5.0e-4, 0.7, 0.5, 1.0e-3, 3.0e-3);
let d = ded.dilution_ratio(8000.0, 2.5e5);
assert!(d > 0.0 && d < 1.0);
}
#[test]
fn test_support_overhang_detection_horizontal() {
let sup = SupportStructure::new(45.0, 0.3, 0.5, 10);
assert!(sup.needs_support([0.0, 0.0, -1.0]));
}
#[test]
fn test_support_overhang_detection_vertical() {
let sup = SupportStructure::new(45.0, 0.3, 0.5, 10);
assert!(!sup.needs_support([1.0, 0.0, 0.0]));
}
#[test]
fn test_jmak_zero_at_t_zero() {
let jmak = JmakTransformation::new(2.5, 0.01);
assert!((jmak.transformed_fraction(0.0) - 0.0).abs() < 1.0e-12);
}
#[test]
fn test_jmak_approaches_unity() {
let jmak = JmakTransformation::new(2.5, 0.01);
let x = jmak.transformed_fraction(1000.0);
assert!(x > 0.99);
}
#[test]
fn test_hillert_grain_growth_increases() {
let hgg = HillertGrainGrowth::new(1.0e-9, 0.5, 100.0e3);
let r0 = 10.0e-6;
let r1 = hgg.grain_radius_after(r0, 1.0, 1500.0);
assert!(r1 >= r0);
}
#[test]
fn test_hall_petch_strength_decreases_with_grain_size() {
let hgg = HillertGrainGrowth::new(1.0e-9, 0.5, 100.0e3);
let jmak = JmakTransformation::new(2.5, 0.01);
let mut me = MicrostructureEvolution::new(1.0e-6, hgg.clone(), jmak.clone());
let hp1 = me.hall_petch_strength(100.0e6, 500.0e3);
me.grain_radius = 10.0e-6;
let hp2 = me.hall_petch_strength(100.0e6, 500.0e3);
assert!(hp1 > hp2);
}
#[test]
fn test_porosity_model_relative_density() {
let mut pm = PorosityModel::new(200.0e9, 450.0e6);
pm.update_from_energy_density(60.0e6, 50.0e6, 150.0e6, 0.001);
assert!((pm.relative_density() + pm.porosity - 1.0).abs() < 1.0e-12);
}
#[test]
fn test_porosity_model_effective_modulus_decreases() {
let mut pm = PorosityModel::new(200.0e9, 450.0e6);
pm.update_from_energy_density(10.0e6, 50.0e6, 150.0e6, 0.001);
let e_eff = pm.effective_young_modulus();
assert!(e_eff < 200.0e9);
}
#[test]
fn test_porosity_model_zero_in_optimal_window() {
let mut pm = PorosityModel::new(200.0e9, 450.0e6);
pm.update_from_energy_density(100.0e6, 50.0e6, 150.0e6, 0.0);
assert!((pm.lof_fraction + pm.keyhole_fraction).abs() < 1.0e-15);
}
#[test]
fn test_distortion_rms_zero_initially() {
let positions = vec![[0.0; 3], [0.01, 0.0, 0.0], [0.0, 0.01, 0.0]];
let dc = DistortionCompensation::new(positions, 1.0);
assert!(dc.rms_distortion() < 1.0e-15);
}
#[test]
fn test_distortion_max_distortion() {
let nominal = vec![[0.0; 3], [1.0, 0.0, 0.0]];
let mut dc = DistortionCompensation::new(nominal, 1.0);
dc.set_distorted_positions(vec![[0.0, 0.0, 0.001], [1.0, 0.0, 0.001]]);
let max_d = dc.max_distortion();
assert!((max_d - 0.001).abs() < 1.0e-12);
}
#[test]
fn test_distortion_compensation_direction() {
let nominal = vec![[0.0; 3]];
let mut dc = DistortionCompensation::new(nominal, 1.0);
dc.set_distorted_positions(vec![[0.0, 0.0, 0.005]]);
let comp = dc.compensated_positions[0];
assert!(comp[2] < 0.0);
}
#[test]
fn test_bpo_optimise_returns_valid_index() {
let weights = OptimizationWeights::uniform();
let candidates = vec![
ProcessParameters::new(150.0, 0.8, 1.2e-4, 5.0e-5),
ProcessParameters::new(200.0, 1.0, 1.0e-4, 5.0e-5),
ProcessParameters::new(250.0, 1.5, 8.0e-5, 5.0e-5),
];
let mut opt = BuildProcessOptimization::new(weights, candidates);
let best = opt.optimise(5.0e9);
assert!(best < 3);
}
#[test]
fn test_bpo_best_parameters_not_none() {
let weights = OptimizationWeights::uniform();
let candidates = vec![ProcessParameters::new(200.0, 1.0, 1.0e-4, 5.0e-5)];
let mut opt = BuildProcessOptimization::new(weights, candidates);
opt.optimise(5.0e9);
assert!(opt.best_parameters().is_some());
}
#[test]
fn test_process_parameters_energy_density() {
let p = ProcessParameters::new(200.0, 1.0, 1.0e-4, 5.0e-5);
let ev = p.energy_density();
let expected = 200.0 / (1.0 * 1.0e-4 * 5.0e-5);
assert!((ev - expected).abs() < 1.0);
}
#[test]
fn test_optimization_weights_normalise() {
let w = OptimizationWeights {
stress_weight: 2.0,
distortion_weight: 2.0,
time_weight: 2.0,
porosity_weight: 2.0,
};
let wn = w.normalise();
let total = wn.stress_weight + wn.distortion_weight + wn.time_weight + wn.porosity_weight;
assert!((total - 1.0).abs() < 1.0e-12);
}
#[test]
fn test_temperature_dependent_conductivity() {
let positions = vec![[0.0; 3]];
let src = GoldakSource::new(200.0, 0.35, 1.5e-4, 1.0e-4, 1.0e-4, 2.0e-4);
let thf = ThermalHistoryFem::new(positions, src, 1923.0, 1723.0, 12.0, 620.0, 8000.0);
let k = thf.temperature_dependent_conductivity(298.15, 1.0e-4);
assert!((k - 12.0).abs() < 1.0e-10);
}
#[test]
fn test_stress_pressure() {
let mut s = StressState::zero();
s.stress[0] = 100.0e6;
s.stress[1] = 200.0e6;
s.stress[2] = 300.0e6;
let p = s.pressure();
assert!((p - 200.0e6).abs() < 1.0);
}
}