#[allow(unused_imports)]
use super::functions::*;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct LayerModel {
pub layer_thickness: f64,
pub hatch_spacing: f64,
pub spot_radius: f64,
pub scan_speed: f64,
pub laser_power: f64,
pub scan_strategy: ScanStrategy,
}
impl LayerModel {
pub fn new(
layer_thickness: f64,
hatch_spacing: f64,
spot_radius: f64,
scan_speed: f64,
laser_power: f64,
scan_strategy: ScanStrategy,
) -> Self {
Self {
layer_thickness,
hatch_spacing,
spot_radius,
scan_speed,
laser_power,
scan_strategy,
}
}
pub fn volumetric_energy_density(&self) -> f64 {
self.laser_power / (self.scan_speed * self.hatch_spacing * self.layer_thickness)
}
pub fn linear_energy_density(&self) -> f64 {
self.laser_power / self.scan_speed
}
pub fn hatch_overlap(&self) -> f64 {
1.0_f64 - self.hatch_spacing / (2.0_f64 * self.spot_radius)
}
pub fn num_tracks(&self, width: f64) -> usize {
((width / self.hatch_spacing).ceil() as usize).max(1)
}
}
#[derive(Debug, Clone)]
pub struct DefectModels {
pub relative_density: f64,
pub energy_density: f64,
pub lof_threshold: f64,
pub keyhole_threshold: f64,
pub uts: f64,
pub fracture_toughness: f64,
pub defect_size: f64,
}
impl DefectModels {
pub fn new(
relative_density: f64,
energy_density: f64,
lof_threshold: f64,
keyhole_threshold: f64,
uts: f64,
fracture_toughness: f64,
defect_size: f64,
) -> Self {
Self {
relative_density,
energy_density,
lof_threshold,
keyhole_threshold,
uts,
fracture_toughness,
defect_size,
}
}
pub fn has_lack_of_fusion(&self) -> bool {
self.energy_density < self.lof_threshold
}
pub fn has_keyhole_porosity(&self) -> bool {
self.energy_density > self.keyhole_threshold
}
pub fn porosity_fraction(&self) -> f64 {
(1.0_f64 - self.relative_density).max(0.0_f64)
}
pub fn cracking_criterion(&self) -> bool {
let k_i = self.uts * (PI * self.defect_size).sqrt();
k_i >= self.fracture_toughness
}
pub fn in_process_window(&self) -> bool {
!self.has_lack_of_fusion() && !self.has_keyhole_porosity()
}
}
#[derive(Debug, Clone)]
pub struct SupportStructure {
pub max_overhang_angle: f64,
pub material_density: f64,
pub support_fill_fraction: f64,
pub support_volume_m3: f64,
}
impl SupportStructure {
pub fn new(
max_overhang_angle_deg: f64,
material_density: f64,
support_fill_fraction: f64,
) -> Self {
Self {
max_overhang_angle: max_overhang_angle_deg.to_radians(),
material_density,
support_fill_fraction: support_fill_fraction.clamp(0.0_f64, 1.0_f64),
support_volume_m3: 0.0_f64,
}
}
pub fn requires_support(&self, normal: [f64; 3]) -> bool {
let nz = normal[2];
if nz >= 0.0_f64 {
return false;
}
let cos_a = (-nz).clamp(-1.0_f64, 1.0_f64);
let angle_from_down = cos_a.acos();
angle_from_down < self.max_overhang_angle
}
pub fn estimate_support(
&mut self,
part_volume_m3: f64,
unsupported_fraction: f64,
part_height_m: f64,
) {
let projected_area = part_volume_m3 / part_height_m.max(1.0e-9_f64);
let support_area = projected_area * unsupported_fraction.clamp(0.0_f64, 1.0_f64);
self.support_volume_m3 =
support_area * (0.5_f64 * part_height_m) * self.support_fill_fraction;
}
pub fn support_mass_kg(&self) -> f64 {
self.support_volume_m3 * self.material_density
}
pub fn waste_fraction(&self, part_mass_kg: f64) -> f64 {
let total = part_mass_kg + self.support_mass_kg();
if total < 1.0e-15_f64 {
return 0.0_f64;
}
self.support_mass_kg() / total
}
}
#[derive(Debug, Clone, Copy)]
pub struct MeltPoolGeometry {
pub half_length: f64,
pub half_width: f64,
pub depth: f64,
}
impl MeltPoolGeometry {
pub fn aspect_ratio(&self) -> f64 {
self.depth / (2.0_f64 * self.half_width)
}
pub fn volume(&self) -> f64 {
(2.0_f64 / 3.0_f64) * PI * self.half_length * self.half_width * self.depth
}
}
#[derive(Debug, Clone)]
pub struct ResidualStress {
pub youngs_modulus: f64,
pub cte: f64,
pub yield_strength: f64,
pub peak_temperature: f64,
pub ambient_temp: f64,
}
impl ResidualStress {
pub fn new(
youngs_modulus: f64,
cte: f64,
yield_strength: f64,
peak_temperature: f64,
ambient_temp: f64,
) -> Self {
Self {
youngs_modulus,
cte,
yield_strength,
peak_temperature,
ambient_temp,
}
}
pub fn longitudinal_stress(&self) -> f64 {
let delta_t = self.peak_temperature - self.ambient_temp;
let elastic = self.youngs_modulus * self.cte * delta_t;
elastic.min(self.yield_strength)
}
pub fn stress_ratio(&self) -> f64 {
self.longitudinal_stress() / self.yield_strength
}
pub fn accumulated_stress(&self, n_layers: usize, layer_fraction: f64) -> f64 {
let sigma_per_layer = self.longitudinal_stress() * layer_fraction;
let total = sigma_per_layer * n_layers as f64;
total.min(self.yield_strength)
}
pub fn delamination_risk(&self) -> bool {
self.longitudinal_stress() > 0.8_f64 * self.yield_strength
}
}
#[derive(Debug, Clone)]
pub struct MultiMaterialAM {
pub e_a: f64,
pub e_b: f64,
pub rho_a: f64,
pub rho_b: f64,
pub k_a: f64,
pub k_b: f64,
pub grading_exponent: f64,
pub grading_height_m: f64,
}
impl MultiMaterialAM {
#[allow(clippy::too_many_arguments)]
pub fn new(
e_a: f64,
e_b: f64,
rho_a: f64,
rho_b: f64,
k_a: f64,
k_b: f64,
grading_exponent: f64,
grading_height_m: f64,
) -> Self {
Self {
e_a,
e_b,
rho_a,
rho_b,
k_a,
k_b,
grading_exponent,
grading_height_m,
}
}
pub fn volume_fraction_b(&self, z_m: f64) -> f64 {
let xi = (z_m / self.grading_height_m.max(1.0e-15_f64)).clamp(0.0_f64, 1.0_f64);
xi.powf(self.grading_exponent)
}
pub fn effective_modulus(&self, z_m: f64) -> f64 {
let vb = self.volume_fraction_b(z_m);
self.e_a * (1.0_f64 - vb) + self.e_b * vb
}
pub fn effective_density(&self, z_m: f64) -> f64 {
let vb = self.volume_fraction_b(z_m);
self.rho_a * (1.0_f64 - vb) + self.rho_b * vb
}
pub fn effective_conductivity(&self, z_m: f64) -> f64 {
let vb = self.volume_fraction_b(z_m);
self.k_a * (1.0_f64 - vb) + self.k_b * vb
}
pub fn average_modulus(&self) -> f64 {
self.e_a + (self.e_b - self.e_a) / (self.grading_exponent + 1.0_f64)
}
}
#[derive(Debug, Clone)]
pub struct GoldakHeatSource {
pub power: f64,
pub absorptivity: f64,
pub a_front: f64,
pub a_rear: f64,
pub b: f64,
pub c: f64,
pub f_front: f64,
}
impl GoldakHeatSource {
pub fn new(power: f64, absorptivity: f64, a_front: f64, a_rear: f64, b: f64, c: f64) -> Self {
Self {
power,
absorptivity,
a_front,
a_rear,
b,
c,
f_front: 0.6_f64,
}
}
fn f_rear(&self) -> f64 {
2.0_f64 - self.f_front
}
pub fn heat_flux(&self, x: f64, y: f64, z: f64) -> f64 {
let q = self.absorptivity * self.power;
let b2 = self.b * self.b;
let c2 = self.c * self.c;
let common = 6.0_f64 * f64::sqrt(3.0_f64) * q;
if x >= 0.0_f64 {
let a2 = self.a_front * self.a_front;
let coeff =
common * self.f_front / (PI * f64::sqrt(PI) * self.a_front * self.b * self.c);
coeff * (-3.0_f64 * x * x / a2 - 3.0_f64 * y * y / b2 - 3.0_f64 * z * z / c2).exp()
} else {
let a2 = self.a_rear * self.a_rear;
let coeff =
common * self.f_rear() / (PI * f64::sqrt(PI) * self.a_rear * self.b * self.c);
coeff * (-3.0_f64 * x * x / a2 - 3.0_f64 * y * y / b2 - 3.0_f64 * z * z / c2).exp()
}
}
pub fn peak_heat_flux(&self) -> f64 {
self.heat_flux(0.0_f64, 0.0_f64, 0.0_f64)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanStrategy {
Raster,
Alternating90,
Rotating67,
Islands,
Chessboard,
}
#[derive(Debug, Clone)]
pub struct AnisotropicAM {
pub build_direction: BuildDirection,
pub e_parallel: f64,
pub e_transverse: f64,
pub sigma_y_parallel: f64,
pub sigma_y_transverse: f64,
pub texture_coefficient: f64,
pub grain_aspect_ratio: f64,
}
impl AnisotropicAM {
pub fn new(
build_direction: BuildDirection,
e_parallel: f64,
e_transverse: f64,
sigma_y_parallel: f64,
sigma_y_transverse: f64,
texture_coefficient: f64,
grain_aspect_ratio: f64,
) -> Self {
Self {
build_direction,
e_parallel,
e_transverse,
sigma_y_parallel,
sigma_y_transverse,
texture_coefficient,
grain_aspect_ratio,
}
}
pub fn anisotropy_index(&self) -> f64 {
(self.e_transverse - self.e_parallel) / self.e_parallel
}
pub fn effective_modulus(&self, theta: f64) -> f64 {
let ct = theta.cos();
let st = theta.sin();
1.0_f64 / (ct * ct / self.e_parallel + st * st / self.e_transverse)
}
pub fn effective_yield_strength(&self, theta: f64) -> f64 {
let ct = theta.cos().abs();
let st = theta.sin().abs();
self.sigma_y_parallel * ct + self.sigma_y_transverse * st
}
}
#[derive(Debug, Clone)]
pub struct DirectedEnergyDeposition {
pub laser_power: f64,
pub powder_feed_rate: f64,
pub scan_speed: f64,
pub spot_radius: f64,
pub catchment_efficiency: f64,
pub substrate_conductivity: f64,
pub absorptivity: f64,
}
impl DirectedEnergyDeposition {
pub fn new(
laser_power: f64,
powder_feed_rate: f64,
scan_speed: f64,
spot_radius: f64,
catchment_efficiency: f64,
substrate_conductivity: f64,
absorptivity: f64,
) -> Self {
Self {
laser_power,
powder_feed_rate,
scan_speed,
spot_radius,
catchment_efficiency,
substrate_conductivity,
absorptivity,
}
}
pub fn effective_power(&self) -> f64 {
self.absorptivity * self.laser_power
}
pub fn clad_height_m(&self, deposit_density: f64) -> f64 {
let width = 2.0_f64 * self.spot_radius;
let mass_flow = self.powder_feed_rate * self.catchment_efficiency;
let volume_per_m = mass_flow / (deposit_density * self.scan_speed * width);
volume_per_m.max(0.0_f64)
}
pub fn dilution_ratio(&self) -> f64 {
let e_s = self.effective_power() / (self.scan_speed * self.spot_radius).max(1.0e-15_f64);
let e_ref = 5.0e6_f64;
1.0_f64 - (-e_s / e_ref).exp()
}
pub fn epitaxial_growth_likely(&self) -> bool {
let g_over_r = self.effective_power()
/ (self.scan_speed.powi(2) * self.spot_radius * self.substrate_conductivity)
.max(1.0e-30_f64);
g_over_r > 1.0e6_f64
}
pub fn utilisation_efficiency(&self) -> f64 {
self.catchment_efficiency.clamp(0.0_f64, 1.0_f64)
}
}
#[derive(Debug, Clone)]
pub struct ThermalHistory {
pub melt_pool: MeltPoolGeometry,
pub peak_temperature: f64,
pub cooling_rate: f64,
pub thermal_gradient: f64,
pub haz_width: f64,
pub recrystallised: bool,
}
impl ThermalHistory {
pub fn from_process(
layer: &LayerModel,
absorptivity: f64,
thermal_conductivity: f64,
density: f64,
specific_heat: f64,
melting_point: f64,
ambient_temp: f64,
) -> Self {
let effective_power = absorptivity * layer.laser_power;
let _diffusivity = thermal_conductivity / (density * specific_heat);
let r = layer.spot_radius.max(1.0e-6_f64);
let peak_temperature =
ambient_temp + effective_power / (2.0_f64 * PI * thermal_conductivity * r);
let thermal_gradient = (peak_temperature - ambient_temp) / r;
let cooling_rate = layer.scan_speed * thermal_gradient;
let q_eff = effective_power;
let half_length = q_eff
/ (2.0_f64 * PI * thermal_conductivity * (peak_temperature - ambient_temp))
.max(1.0e-20_f64);
let half_width = half_length * 0.6_f64;
let depth = half_length * 0.4_f64;
let haz_width = half_width * 1.5_f64;
let recrystallised = peak_temperature > 0.5_f64 * melting_point;
ThermalHistory {
melt_pool: MeltPoolGeometry {
half_length,
half_width,
depth,
},
peak_temperature,
cooling_rate,
thermal_gradient,
haz_width,
recrystallised,
}
}
pub fn solidification_rate(&self) -> f64 {
if self.thermal_gradient.abs() < 1.0e-12_f64 {
0.0_f64
} else {
self.cooling_rate / self.thermal_gradient
}
}
pub fn primary_dendrite_arm_spacing(&self) -> f64 {
let a = 80.0e-6_f64;
let gr = self.thermal_gradient * self.solidification_rate();
if gr < 1.0e-20_f64 {
a
} else {
a * gr.powf(-0.25_f64)
}
}
}
#[derive(Debug, Clone)]
pub struct BinderJetting {
pub layer_thickness_m: f64,
pub packing_density: f64,
pub binder_saturation: f64,
pub particle_diameter_m: f64,
pub sintering_shrinkage: f64,
pub green_density: f64,
}
impl BinderJetting {
pub fn new(
layer_thickness_m: f64,
packing_density: f64,
binder_saturation: f64,
particle_diameter_m: f64,
sintering_shrinkage: f64,
) -> Self {
let green_density = packing_density * binder_saturation.clamp(0.0_f64, 1.0_f64);
Self {
layer_thickness_m,
packing_density: packing_density.clamp(0.0_f64, 1.0_f64),
binder_saturation: binder_saturation.clamp(0.0_f64, 1.0_f64),
particle_diameter_m,
sintering_shrinkage: sintering_shrinkage.clamp(0.0_f64, 0.5_f64),
green_density,
}
}
pub fn void_fraction(&self) -> f64 {
1.0_f64 - self.packing_density
}
pub fn binder_volume_fraction(&self) -> f64 {
self.void_fraction() * self.binder_saturation
}
pub fn sintered_dimension(&self, green_dimension_m: f64) -> f64 {
green_dimension_m * (1.0_f64 - self.sintering_shrinkage)
}
pub fn volumetric_shrinkage(&self) -> f64 {
let s = self.sintering_shrinkage;
1.0_f64 - (1.0_f64 - s).powi(3)
}
pub fn max_spreading_speed_m_s(&self) -> f64 {
let k_kozeny = 5.0_f64;
let porosity = self.void_fraction().max(0.01_f64);
let d = self.particle_diameter_m.max(1.0e-9_f64);
porosity.powi(3) / (k_kozeny * (1.0_f64 - porosity).powi(2) * d)
}
}
#[derive(Debug, Clone)]
pub struct RosenthalSolution {
pub power: f64,
pub absorptivity: f64,
pub scan_speed: f64,
pub thermal_conductivity: f64,
pub diffusivity: f64,
pub ambient_temp: f64,
}
impl RosenthalSolution {
pub fn new(
power: f64,
absorptivity: f64,
scan_speed: f64,
thermal_conductivity: f64,
diffusivity: f64,
ambient_temp: f64,
) -> Self {
Self {
power,
absorptivity,
scan_speed,
thermal_conductivity,
diffusivity,
ambient_temp,
}
}
pub fn temperature(&self, xi: f64, y: f64, z: f64) -> f64 {
let r = (xi * xi + y * y + z * z).sqrt();
if r < 1.0e-15_f64 {
return f64::INFINITY;
}
let q = self.absorptivity * self.power;
let v = self.scan_speed;
let alpha = self.diffusivity;
let k = self.thermal_conductivity;
let exponent = -v / (2.0_f64 * alpha) * (r + xi);
self.ambient_temp + q / (2.0_f64 * PI * k * r) * exponent.exp()
}
pub fn melt_pool_width(&self, t_melt: f64) -> f64 {
let mut lo = 1.0e-9_f64;
let mut hi = 1.0e-2_f64;
for _ in 0..60 {
let mid = 0.5_f64 * (lo + hi);
let t = self.temperature(0.0_f64, mid, 0.0_f64);
if t > t_melt {
lo = mid;
} else {
hi = mid;
}
}
2.0_f64 * 0.5_f64 * (lo + hi)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildDirection {
PlusZ,
Tilted45X,
Horizontal,
Custom,
}
#[derive(Debug, Clone)]
pub struct ProcessWindow {
pub theoretical_density: f64,
pub e_low: f64,
pub e_high: f64,
pub hardness_full_dense: f64,
pub hall_petch_k: f64,
pub grain_size_opt: f64,
}
impl ProcessWindow {
pub fn new(
theoretical_density: f64,
e_low: f64,
e_high: f64,
hardness_full_dense: f64,
hall_petch_k: f64,
grain_size_opt: f64,
) -> Self {
Self {
theoretical_density,
e_low,
e_high,
hardness_full_dense,
hall_petch_k,
grain_size_opt,
}
}
pub fn relative_density(&self, energy_density: f64) -> f64 {
let e_mid = 0.5_f64 * (self.e_low + self.e_high);
let k = 5.0_f64 / (self.e_high - self.e_low).max(1.0_f64);
1.0_f64 / (1.0_f64 + (-k * (energy_density - e_mid)).exp())
}
pub fn hardness_prediction(&self, energy_density: f64, grain_size: f64) -> f64 {
let rel = self.relative_density(energy_density);
let hp = self.hall_petch_k / grain_size.sqrt();
rel * (self.hardness_full_dense + hp - self.hall_petch_k / self.grain_size_opt.sqrt())
}
pub fn is_in_window(&self, energy_density: f64) -> bool {
energy_density >= self.e_low && energy_density <= self.e_high
}
pub fn window_width(&self) -> f64 {
(self.e_high - self.e_low).max(0.0_f64)
}
}
#[derive(Debug, Clone)]
pub struct DistortionCompensation {
pub scale_factor: f64,
pub max_deformation_m: f64,
pub tolerance_m: f64,
pub iteration_count: usize,
}
impl DistortionCompensation {
pub fn new(scale_factor: f64, max_deformation_m: f64, tolerance_m: f64) -> Self {
Self {
scale_factor,
max_deformation_m,
tolerance_m,
iteration_count: 0,
}
}
pub fn apply(&self, displacements_m: &[f64]) -> Vec<f64> {
displacements_m
.iter()
.map(|d| {
let comp = self.scale_factor * d;
comp.clamp(-self.max_deformation_m, self.max_deformation_m)
})
.collect()
}
pub fn iterate(&mut self, initial_displacements: &[f64], max_iter: usize) -> Vec<f64> {
let mut current = initial_displacements.to_vec();
self.iteration_count = 0;
for _i in 0..max_iter {
let next = self.apply(¤t);
let rms: f64 = next
.iter()
.zip(current.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
/ (next.len().max(1) as f64);
current = next;
self.iteration_count += 1;
if rms.sqrt() < self.tolerance_m {
break;
}
}
current
}
pub fn rms_displacement(displacements_m: &[f64]) -> f64 {
if displacements_m.is_empty() {
return 0.0_f64;
}
let sum_sq: f64 = displacements_m.iter().map(|d| d * d).sum();
(sum_sq / displacements_m.len() as f64).sqrt()
}
}
#[derive(Debug, Clone)]
pub struct ProcessWindowOptimizer {
pub power_min: f64,
pub power_max: f64,
pub speed_min: f64,
pub speed_max: f64,
pub layer_thickness: f64,
pub hatch_spacing: f64,
pub e_low: f64,
pub e_high: f64,
}
impl ProcessWindowOptimizer {
#[allow(clippy::too_many_arguments)]
pub fn new(
power_min: f64,
power_max: f64,
speed_min: f64,
speed_max: f64,
layer_thickness: f64,
hatch_spacing: f64,
e_low: f64,
e_high: f64,
) -> Self {
Self {
power_min,
power_max,
speed_min,
speed_max,
layer_thickness,
hatch_spacing,
e_low,
e_high,
}
}
pub fn energy_density(&self, power: f64, speed: f64) -> f64 {
power / (speed * self.hatch_spacing * self.layer_thickness).max(1.0e-30_f64)
}
pub fn porosity_score(&self, ev: f64) -> f64 {
if ev < self.e_low {
((self.e_low - ev) / self.e_low).powi(2)
} else if ev > self.e_high {
((ev - self.e_high) / self.e_high).powi(2)
} else {
0.0_f64
}
}
pub fn roughness_score(&self, speed: f64) -> f64 {
let v_ref = 0.8_f64 * self.speed_max;
if speed > v_ref {
((speed - v_ref) / (self.speed_max - v_ref + 1.0e-30_f64)).powi(2)
} else {
0.0_f64
}
}
pub fn cost(&self, power: f64, speed: f64) -> f64 {
let ev = self.energy_density(power, speed);
self.porosity_score(ev) + 0.5_f64 * self.roughness_score(speed)
}
pub fn optimise(&self, n_points: usize) -> (f64, f64) {
let n = n_points.max(2);
let mut best_cost = f64::INFINITY;
let mut best_power = self.power_min;
let mut best_speed = self.speed_min;
for ip in 0..n {
let p = self.power_min + ip as f64 * (self.power_max - self.power_min) / (n - 1) as f64;
for iv in 0..n {
let v =
self.speed_min + iv as f64 * (self.speed_max - self.speed_min) / (n - 1) as f64;
let c = self.cost(p, v);
if c < best_cost {
best_cost = c;
best_power = p;
best_speed = v;
}
}
}
(best_power, best_speed)
}
}