diff --git a/.github/workflows/rustbca_compile_check.yml b/.github/workflows/rustbca_compile_check.yml index bec6e7f..5cd3005 100644 --- a/.github/workflows/rustbca_compile_check.yml +++ b/.github/workflows/rustbca_compile_check.yml @@ -1,4 +1,4 @@ -name: rustBCA Compile check +name: RustBCA Compile check on: push: @@ -41,11 +41,11 @@ jobs: - name: Run Examples run: | cargo run --release 0D examples/boron_nitride_0D.toml - ./target/release/rustBCA 0D examples/titanium_dioxide_0D.toml - ./target/release/rustBCA 1D examples/layered_geometry_1D.toml + ./target/release/RustBCA 0D examples/titanium_dioxide_0D.toml + ./target/release/RustBCA 1D examples/layered_geometry_1D.toml cat 2000.0eV_0.0001deg_He_TiO2_Al_Sisummary.output - ./target/release/rustBCA examples/boron_nitride.toml - ./target/release/rustBCA examples/layered_geometry.toml + ./target/release/RustBCA examples/boron_nitride.toml + ./target/release/RustBCA examples/layered_geometry.toml cat 2000.0eV_0.0001deg_He_TiO2_Al_Sisummary.output - ./target/release/rustBCA SPHERE examples/boron_nitride_sphere.toml + ./target/release/RustBCA SPHERE examples/boron_nitride_sphere.toml cargo run --release --features parry3d TRIMESH examples/tungsten_twist_trimesh.toml diff --git a/Cargo.toml b/Cargo.toml index 06a44b9..6345365 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,44 +1,53 @@ -[package] -name = "rustBCA" -version = "1.1.0" -authors = ["Jon Drobny "] -edition = "2018" - -[[bin]] -name="rustBCA" - -[dependencies] -rand = "0.8.3" -toml = "0.5.8" -anyhow = "1.0.38" -itertools = "0.10.0" -rayon = "1.5.0" -geo = {version = "0.17.1", optional = false} -indicatif = {version = "0.15.0", features=["rayon"]} -serde = { version = "1.0.123", features = ["derive"] } -hdf5 = {version = "0.7.1", optional = true} -openblas-src = {version = "0.9", optional = true} -netlib-src = {version = "0.8", optional = true} -intel-mkl-src = {version = "0.6.0", optional = true} -rcpr = { git = "https://github.com/drobnyjt/rcpr", optional = true} -ndarray = {version = "0.14.0", features = ["serde"], optional = true} -parry3d-f64 = {version = "0.2.0", optional = true} - -[dev-dependencies] -float-cmp = "0.8.0" - -[profile.release] -lto = "fat" -codegen-units = 1 -opt-level = 3 -debug = false - -[features] -hdf5_input = ["hdf5"] -cpr_rootfinder_openblas = ["rcpr", "openblas-src"] -cpr_rootfinder_netlib = ["rcpr", "netlib-src"] -cpr_rootfinder_intel_mkl = ["rcpr", "intel-mkl-src"] -distributions = ["ndarray"] -no_list_output = [] -parry3d = ["parry3d-f64"] -accelerated_ions = [] +[package] +name = "RustBCA" +version = "1.1.0" +authors = ["Jon Drobny "] +edition = "2018" + +[[bin]] +name="RustBCA" + +[lib] +name = "libRustBCA" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +rand = "0.8.3" +toml = "0.5.8" +anyhow = "1.0.38" +itertools = "0.10.0" +rayon = "1.5.0" +geo = {version = "0.17.1", optional = false} +indicatif = {version = "0.15.0", features=["rayon"]} +serde = { version = "1.0.123", features = ["derive"] } +hdf5 = {version = "0.7.1", optional = true} +openblas-src = {version = "0.9", optional = true} +netlib-src = {version = "0.8", optional = true} +intel-mkl-src = {version = "0.6.0", optional = true} +rcpr = { git = "https://github.com/drobnyjt/rcpr", optional = true} +ndarray = {version = "0.14.0", features = ["serde"], optional = true} +parry3d-f64 = {version = "0.2.0", optional = true} + +[dependencies.pyo3] +version = "0.13.2" +features = ["extension-module"] + +[dev-dependencies] +float-cmp = "0.8.0" + +[profile.release] +lto = "fat" +codegen-units = 1 +opt-level = 3 +debug = false + +[features] +hdf5_input = ["hdf5"] +cpr_rootfinder_openblas = ["rcpr", "openblas-src"] +cpr_rootfinder_netlib = ["rcpr", "netlib-src"] +cpr_rootfinder_intel_mkl = ["rcpr", "intel-mkl-src"] +distributions = ["ndarray"] +no_list_output = [] +parry3d = ["parry3d-f64"] +accelerated_ions = [] diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7c68298 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include Cargo.toml +recursive-include src * diff --git a/RustBCA.h b/RustBCA.h new file mode 100644 index 0000000..72ea4ae --- /dev/null +++ b/RustBCA.h @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include + +static const double PI = M_PI; + +static const double FRAC_2_SQRT_PI = M_2_PI; + +static const double SQRT_2 = M_SQRT2; + +///Fundamental charge in Coulombs. +static const double Q = 1.602176634e-19; + +/// One electron-volt in Joules. +static const double EV = Q; + +/// One atomic mass unit in kilograms. +static const double AMU = 1.66053906660e-27; + +/// One Angstrom in meters. +static const double ANGSTROM = 1e-10; + +/// One micron in meters. +static const double MICRON = 1e-6; + +/// One nanometer in meters. +static const double NM = 1e-9; + +/// One centimeter in meters. +static const double CM = 1e-2; + +/// Vacuum permitivity in Farads/meter. +static const double EPS0 = 8.8541878128e-12; + +/// Bohr radius in meters. +static const double A0 = 5.29177210903e-11; + +/// Electron mass in kilograms. +static const double ME = 9.1093837015e-31; + +/// sqrt(pi). +static const double SQRTPI = (2. / FRAC_2_SQRT_PI); + +/// sqrt(2 * pi). +static const double SQRT2PI = ((2. * SQRT_2) / FRAC_2_SQRT_PI); + +/// Speed of light in meters/second. +static const double C = 299792458.; + +/// Bethe-Bloch electronic stopping prefactor, in SI units. +static const double BETHE_BLOCH_PREFACTOR = ((((((4. * PI) * ((Q * Q) / ((4. * PI) * EPS0))) * ((Q * Q) / ((4. * PI) * EPS0))) / ME) / C) / C); + +/// Lindhard-Scharff electronic stopping prefactor, in SI units. +static const double LINDHARD_SCHARFF_PREFACTOR = (((1.212 * ANGSTROM) * ANGSTROM) * Q); + +/// Lindhard reduced energy prefactor, in SI units. +static const double LINDHARD_REDUCED_ENERGY_PREFACTOR = ((((4. * PI) * EPS0) / Q) / Q); + +struct OutputBCA { + uintptr_t len; + double (*particles)[9]; +}; + +extern "C" { + +OutputBCA simple_bca_c(double x, + double y, + double z, + double ux, + double uy, + double uz, + double E1, + double Z1, + double m1, + double Ec1, + double Es1, + double Z2, + double m2, + double Ec2, + double Es2, + double n2, + double Eb2); + +} // extern "C" diff --git a/cbindgen.toml b/cbindgen.toml new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9203538 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools", "setuptools-rust"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6e08107 --- /dev/null +++ b/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup +from setuptools_rust import Binding, RustExtension + +setup( + name="RustBCA", + rust_extensions=[RustExtension("libRustBCA.pybca", binding=Binding.PyO3)], + # rust extensions are not zip safe, just like C-extensions. + zip_safe=False, +) diff --git a/src/bca.rs b/src/bca.rs index 8ef3c9b..23b4be0 100644 --- a/src/bca.rs +++ b/src/bca.rs @@ -1,767 +1,767 @@ -use super::*; - -#[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] -use rcpr::chebyshev::*; - -/// Geometrical quantities of binary collision. -pub struct BinaryCollisionGeometry { - pub phi_azimuthal: f64, - pub impact_parameter: f64, - pub mfp: f64 -} - -impl BinaryCollisionGeometry { - /// Constructs a new binary collision geometry object. - pub fn new(phi_azimuthal: f64, impact_parameter: f64, mfp: f64) -> BinaryCollisionGeometry { - BinaryCollisionGeometry { - phi_azimuthal, - impact_parameter, - mfp - } - } -} - -impl fmt::Display for BinaryCollisionGeometry { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Binary collision geometry. \n phi_azimuthal = {} \n p = {} \n mfp = {} \n", - self.phi_azimuthal, self.impact_parameter, self.mfp) - } -} - -/// Resultant quantities of a binary collision. -pub struct BinaryCollisionResult { - pub theta: f64, - pub psi: f64, - pub psi_recoil: f64, - pub recoil_energy: f64, - pub asymptotic_deflection: f64, - pub normalized_distance_of_closest_approach: f64 -} - -impl BinaryCollisionResult { - - /// Constructs a new binary collision result object. - pub fn new(theta: f64, psi: f64, psi_recoil: f64, recoil_energy: f64, - asymptotic_deflection: f64, normalized_distance_of_closest_approach: f64) -> BinaryCollisionResult { - BinaryCollisionResult { - theta, - psi, - psi_recoil, - recoil_energy, - asymptotic_deflection, - normalized_distance_of_closest_approach - } - } -} - -impl fmt::Display for BinaryCollisionResult { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Binary collision result. \n theta = {} \n psi = {} \n psi_r = {} \n E_recoil = {} \n tau = {} \n x0 = {} \n", - self.theta, self.psi, self.psi_recoil, self.recoil_energy, self.asymptotic_deflection, self.normalized_distance_of_closest_approach) - } -} - -/// This function takes a single particle, a material, and an options object and runs a binary-collision-approximation trajectory for that particle in that material, producing a final particle list that consists of the original ion and any material particles displaced thereby. -pub fn single_ion_bca(particle: particle::Particle, material: &material::Material, options: &Options) -> Vec { - - let mut particles: Vec = Vec::new(); - particles.push(particle); - - let mut particle_output: Vec = Vec::new(); - - let mut particle_index = particles.len(); - - while particle_index > 0 { - - //Remove particle from top of vector as particle_1 - let mut particle_1 = particles.pop().unwrap(); - - //println!("Particle start Z: {} E: {} ({}, {}, {})", particle_1.Z, particle_1.E/Q, particle_1.pos.x/ANGSTROM, particle_1.pos.y/ANGSTROM, particle_1.pos.z/ANGSTROM); - - //BCA loop - while !particle_1.stopped & !particle_1.left { - - //Choose impact parameters and azimuthal angles for all collisions, and determine mean free path - let binary_collision_geometries = bca::determine_mfp_phi_impact_parameter(&mut particle_1, &material, &options); - - #[cfg(feature = "accelerated_ions")] - let distance_to_target = if !material.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z) { - let (x, y, z) = material.geometry.closest_point(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - ((x - particle_1.pos.x).powi(2) + (y - particle_1.pos.y).powi(2) + (z - particle_1.pos.z).powi(2)).sqrt() - } else { - 0. - }; - - let mut total_energy_loss = 0.; - let mut total_asymptotic_deflection = 0.; - let mut normalized_distance_of_closest_approach = 0.; - let mut strong_collision_Z = 0.; - let mut strong_collision_index: usize = 0; - - //Collision loop - for (k, binary_collision_geometry) in binary_collision_geometries.iter().enumerate().take(options.weak_collision_order + 1) { - - let (species_index, mut particle_2) = bca::choose_collision_partner(&particle_1, &material, - &binary_collision_geometry, &options); - - //If recoil location is inside, proceed with binary collision loop - if material.inside(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z) & material.inside_energy_barrier(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z) { - - //Determine scattering angle from binary collision - let binary_collision_result = bca::calculate_binary_collision(&particle_1, - &particle_2, &binary_collision_geometry, &options) - .with_context(|| format!("Numerical error: binary collision calculation failed at x = {} y = {} with {}", - particle_1.pos.x, particle_2.pos.x, &binary_collision_geometry)) - .unwrap(); - - //println!("{}", binary_collision_result); - - //Only use 0th order collision for local electronic stopping - if k == 0 { - normalized_distance_of_closest_approach = binary_collision_result.normalized_distance_of_closest_approach; - strong_collision_Z = particle_2.Z; - strong_collision_index = species_index; - } - - //Energy transfer to recoil - particle_2.E = binary_collision_result.recoil_energy - material.average_bulk_binding_energy(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z); - particle_2.energy_origin = particle_2.E; - - //Accumulate asymptotic deflections for primary particle - total_energy_loss += binary_collision_result.recoil_energy; - - //total_deflection_angle += psi; - total_asymptotic_deflection += binary_collision_result.asymptotic_deflection; - - //Rotate particle 1, 2 by lab frame scattering angles - particle::rotate_particle(&mut particle_1, binary_collision_result.psi, - binary_collision_geometry.phi_azimuthal); - - particle::rotate_particle(&mut particle_2, -binary_collision_result.psi_recoil, - binary_collision_geometry.phi_azimuthal); - - particle_2.dir_old.x = particle_2.dir.x; - particle_2.dir_old.y = particle_2.dir.y; - particle_2.dir_old.z = particle_2.dir.z; - - //Only track number of strong collisions, i.e., k = 0 - if (binary_collision_result.psi > 0.) & (k == 0) { - particle_1.number_collision_events += 1; - } - - //Deep recoil suppression - //See Eckstein 1991 7.5.3 for recoil suppression function - if options.track_recoils & options.suppress_deep_recoils { - let E = particle_1.E; - let Za: f64 = particle_1.Z; - let Zb: f64 = particle_2.Z; - - let Ma: f64 = particle_1.m; - let Mb: f64 = particle_2.m; - - let n = material.total_number_density(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z); - //We just need the lindhard screening length here, so the particular potential is not important - let a: f64 = interactions::screening_length(Za, Zb, InteractionPotential::MOLIERE); - let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E; - let estimated_range_of_recoils = (reduced_energy.powf(0.3) + 0.1).powi(3)/n/a/a; - - let (x2, y2, z2) = material.closest_point(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z); - let dx = x2 - particle_2.pos.x; - let dy = y2 - particle_2.pos.y; - let dz = z2 - particle_2.pos.z; - let distance_to_surface = (dx*dx + dy*dy + dz*dz).sqrt(); - - if (distance_to_surface < estimated_range_of_recoils) & (particle_2.E > particle_2.Ec) { - particles.push(particle_2); - } - - //If transferred energy > cutoff energy, add recoil to particle vector - } else if options.track_recoils & (particle_2.E > particle_2.Ec) { - particles.push(particle_2); - } - } - } - - //Advance particle in space and track total distance traveled - #[cfg(not(feature = "accelerated_ions"))] - let distance_traveled = particle::particle_advance(&mut particle_1, - binary_collision_geometries[0].mfp, total_asymptotic_deflection); - - #[cfg(feature = "accelerated_ions")] - let distance_traveled = particle::particle_advance(&mut particle_1, - binary_collision_geometries[0].mfp + distance_to_target, total_asymptotic_deflection); - - //Subtract total energy from all simultaneous collisions and electronic stopping - bca::update_particle_energy(&mut particle_1, &material, distance_traveled, - total_energy_loss, normalized_distance_of_closest_approach, strong_collision_Z, - strong_collision_index, &options); - //println!("Particle finished collision loop Z: {} E: {} ({}, {}, {})", particle_1.Z, particle_1.E/Q, particle_1.pos.x/ANGSTROM, particle_1.pos.y/ANGSTROM, particle_1.pos.z/ANGSTROM); - - //println!("{} {} {}", energy_0/EV, energy_1/EV, (energy_1 - energy_0)/EV); - - //Check boundary conditions on leaving and stopping - material::boundary_condition_planar(&mut particle_1, &material); - - //Set particle index to topmost particle - particle_index = particles.len(); - } - //println!("Particle stopped or left Z: {} E: {} ({}, {}, {})", particle_1.Z, particle_1.E/Q, particle_1.pos.x/ANGSTROM, particle_1.pos.y/ANGSTROM, particle_1.pos.z/ANGSTROM); - particle_output.push(particle_1); - } - particle_output -} - -/// For a particle in a material, determine the mean free path and choose the azimuthal angle and impact parameter. -/// The mean free path can be exponentially distributed (gaseous) or constant (amorphous solid/liquid). Azimuthal angles are chosen uniformly. Impact parameters are chosen for collision partners distributed uniformly on a disk of density-dependent radius. -pub fn determine_mfp_phi_impact_parameter(particle_1: &mut particle::Particle, material: &material::Material, options: &Options) -> Vec { - - let x = particle_1.pos.x; - let y = particle_1.pos.y; - let z = particle_1.pos.z; - - let mut mfp = material.mfp(x, y, z); - - let mut phis_azimuthal = Vec::with_capacity(options.weak_collision_order + 1); - let mut binary_collision_geometries = Vec::with_capacity(options.weak_collision_order + 1); - - //Each weak collision gets its own aziumuthal angle in annuli around collision point - for k in 0..options.weak_collision_order + 1 { - phis_azimuthal.push(2.*PI*rand::random::()); - } - - if options.high_energy_free_flight_paths { - - let Ma: f64 = particle_1.m; - let Mb: f64 = material.average_mass(x, y, z); - let Za: f64 = particle_1.Z; - let Zb: f64 = material.average_Z(x, y, z); - let n: &Vec = material.number_densities(x, y, z); - let ck: f64 = material.electronic_stopping_correction_factor(x, y, z); - let E: f64 = particle_1.E; - let Ec: f64 = particle_1.Ec; - //We just need the Lindhard screening length here, so the particular potential is not important - let a: f64 = interactions::screening_length(Za, Zb, InteractionPotential::MOLIERE); - let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E; - - //Minimum energy transfer for generating scattering event set to cutoff energy - let E_min = Ec*(Ma + Mb)*(Ma + Mb)/4./Ma/Mb; - let reduced_energy_min: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E_min; - - //Free flight path formulation here described in SRIM textbook chapter 7, and Eckstein 1991 7.5.2 - let ep = (reduced_energy*reduced_energy_min).sqrt(); - let mut pmax = a/(ep + ep.sqrt() + 0.125*ep.powf(0.1)); - let mut ffp = 1./(material.total_number_density(x, y, z)*pmax*pmax*PI); - let stopping_powers = material.electronic_stopping_cross_sections(particle_1, ElectronicStoppingMode::INTERPOLATED); - - let delta_energy_electronic = stopping_powers.iter().zip(material.number_densities(x, y, z)).map(|(&se, number_density)| se*number_density).sum::()*ffp*ck; - - //If losing too much energy, scale free-flight-path down - //5 percent limit set in original TRIM paper, Biersack and Haggmark 1980 - if delta_energy_electronic > 0.05*E { - ffp *= 0.05*E/delta_energy_electronic; - pmax = (1./(material.total_number_density(x, y, z)*PI*ffp)).sqrt() - } - - //If free-flight-path less than the interatomic spacing, revert to amorphous solid model - //Mentioned in Eckstein 1991, Ziegler, Biersack, and Ziegler 2008 (SRIM textbook 7-8) - if ffp < mfp { - - ffp = mfp; - //Cylindrical geometry - pmax = mfp/SQRT2PI; - let mut impact_parameter = Vec::with_capacity(1); - let random_number = rand::random::(); - let p = pmax*random_number.sqrt(); - impact_parameter.push(p); - - //Atomically rough surface - scatter initial collisions using mfp near interface - if particle_1.first_step { - ffp = mfp*rand::random::(); - particle_1.first_step = false; - } - - ffp *= match options.mean_free_path_model { - MeanFreePathModel::GASEOUS => -rand::random::().ln(), - MeanFreePathModel::LIQUID => 1.0, - MeanFreePathModel::THRESHOLD{density} => { - if material.geometry.get_densities(x, y, z).iter().sum::() < density { - -rand::random::().ln() - } else { - 1.0 - } - } - }; - - binary_collision_geometries.push(BinaryCollisionGeometry::new(phis_azimuthal[0], impact_parameter[0], ffp)); - return binary_collision_geometries; - - } else { - - //Impact parameter chosen as sqrt(-ln(R))*pmax, as in Biersack and Haggmark 1980, - //And Mendenhall Weller 2005 - //And SRIM textbook chapter 7 - //And Eckstein 1991 - let mut impact_parameter = Vec::with_capacity(1); - let random_number = rand::random::(); - let p = pmax*(-random_number.ln()).sqrt(); - impact_parameter.push(p); - - //Atomically rough surface - scatter initial collisions using mfp near interface - if particle_1.first_step { - ffp = mfp*rand::random::(); - particle_1.first_step = false; - } - - if options.mean_free_path_model == MeanFreePathModel::GASEOUS { - ffp *= -rand::random::().ln(); - } - - binary_collision_geometries.push(BinaryCollisionGeometry::new(phis_azimuthal[0], impact_parameter[0], ffp)); - return binary_collision_geometries; - } - - } else { - - //If not using free flight paths, use weak collision model - let pmax = mfp/SQRTPI; - - //Cylindrical geometry - let mut impact_parameters = Vec::with_capacity(options.weak_collision_order + 1); - for k in 0..(options.weak_collision_order + 1) { - let random_number = rand::random::(); - let p = pmax*(random_number + k as f64).sqrt(); - impact_parameters.push(p) - } - - //Atomically rough surface - scatter initial collisions - if particle_1.first_step { - mfp *= rand::random::(); - particle_1.first_step = false; - } - - if options.mean_free_path_model == MeanFreePathModel::GASEOUS { - mfp *= -rand::random::().ln(); - } - - - for k in 0..(options.weak_collision_order + 1) { - binary_collision_geometries.push(BinaryCollisionGeometry::new(phis_azimuthal[k], impact_parameters[k], mfp)) - } - - return binary_collision_geometries; - } -} - -/// For a particle in a material, and for a particular binary collision geometry, choose a species for the collision partner. -pub fn choose_collision_partner(particle_1: &particle::Particle, material: &material::Material, binary_collision_geometry: &BinaryCollisionGeometry, options: &Options) -> (usize, particle::Particle) { - let x = particle_1.pos.x; - let y = particle_1.pos.y; - let z = particle_1.pos.z; - - let impact_parameter = binary_collision_geometry.impact_parameter; - let mfp = binary_collision_geometry.mfp; - let phi_azimuthal = binary_collision_geometry.phi_azimuthal; - - //Determine cosines and sines - let sinphi: f64 = phi_azimuthal.sin(); - let cosx: f64 = particle_1.dir.x; - let cosy: f64 = particle_1.dir.y; - let cosz: f64 = particle_1.dir.z; - let sinx: f64 = (1. - cosx*cosx).sqrt(); - let cosphi: f64 = phi_azimuthal.cos(); - - //Find recoil location - let x_recoil: f64 = x + mfp*cosx - impact_parameter*cosphi*sinx; - let y_recoil: f64 = y + mfp*cosy - impact_parameter*(sinphi*cosz - cosphi*cosy*cosx)/sinx; - let z_recoil: f64 = z + mfp*cosz + impact_parameter*(sinphi*cosy - cosphi*cosx*cosz)/sinx; - - //Choose recoil Z, M - let (species_index, Z_recoil, M_recoil, Ec_recoil, Es_recoil, interaction_index) = material.choose(x_recoil, y_recoil, z_recoil); - - return (species_index, - particle::Particle::new( - M_recoil, Z_recoil, 0., Ec_recoil, Es_recoil, - x_recoil, y_recoil, z_recoil, - cosx, cosy, cosz, - false, options.track_recoil_trajectories, interaction_index - ) - ) -} - -/// Calculate the distance of closest approach of two particles given a particular binary collision geometry. -fn distance_of_closest_approach(particle_1: &particle::Particle, particle_2: &particle::Particle, binary_collision_geometry: &BinaryCollisionGeometry, options: &Options) -> f64 { - let Za: f64 = particle_1.Z; - let Zb: f64 = particle_2.Z; - let Ma: f64 = particle_1.m; - let Mb: f64 = particle_2.m; - let E0: f64 = particle_1.E; - let relative_energy = E0*Mb/(Ma + Mb); - let p = binary_collision_geometry.impact_parameter; - - let interaction_potential = options.interaction_potential[particle_1.interaction_index][particle_2.interaction_index]; - - if let InteractionPotential::COULOMB{Za: Z1, Zb: Z2} = interaction_potential { - let doca = Z1*Z2*Q*Q/relative_energy/PI/EPS0/8. + (64.*(relative_energy*PI*p*EPS0).powi(2) + (Z1*Z2*Q*Q).powi(2)).sqrt()/relative_energy/PI/EPS0/8.; - return doca/interactions::screening_length(Z1, Z2, interaction_potential); - } - - let root_finder = if relative_energy < interactions::energy_threshold_single_root(interaction_potential) { - options.root_finder[particle_1.interaction_index][particle_2.interaction_index] - } else {Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-6}}; - - #[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] - match root_finder { - Rootfinder::POLYNOMIAL{complex_threshold} => polynomial_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, complex_threshold) - .with_context(|| format!("Numerical error: Polynomial rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) - .unwrap(), - Rootfinder::CPR{n0, nmax, epsilon, complex_threshold, truncation_threshold, far_from_zero, interval_limit, derivative_free} => - cpr_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, n0, nmax, epsilon, complex_threshold, truncation_threshold, far_from_zero, interval_limit, derivative_free) - .with_context(|| format!("Numerical error: CPR rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) - .unwrap(), - Rootfinder::NEWTON{max_iterations, tolerance} => newton_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, max_iterations, tolerance) - .with_context(|| format!("Numerical error: Newton rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) - .unwrap(), - Rootfinder::DEFAULTNEWTON => newton_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, 100, 1E-3) - .with_context(|| format!("Numerical error: Newton rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) - .unwrap(), - } - - #[cfg(not(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl")))] - match root_finder { - Rootfinder::NEWTON{max_iterations, tolerance} => newton_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, max_iterations, tolerance) - .with_context(|| format!("Numerical error: Newton rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) - .unwrap(), - Rootfinder::DEFAULTNEWTON => newton_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, 100, 1E-3) - .with_context(|| format!("Numerical error: Newton rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) - .unwrap(), - _ => panic!("Input error: unimplemented root-finder. Choose NEWTON or build with cpr_rootfinder to enable CPR and POLYNOMIAL") - } -} - -/// Update a particle's energy following nuclear and electronic interactions of a single BCA step. -pub fn update_particle_energy(particle_1: &mut particle::Particle, material: &material::Material, distance_traveled: f64, - recoil_energy: f64, x0: f64, strong_collision_Z: f64, strong_collision_index: usize, options: &Options) { - - //If particle energy drops below zero before electronic stopping calcualtion, it produces NaNs - particle_1.E -= recoil_energy; - assert!(!particle_1.E.is_nan(), "Numerical error: particle energy is NaN following collision."); - if particle_1.E < 0. { - particle_1.E = 0.; - } - - let x = particle_1.pos.x; - let y = particle_1.pos.y; - let z = particle_1.pos.z; - - if material.inside(x, y, z) { - - let interaction_potential = options.interaction_potential[particle_1.interaction_index][material.interaction_index[strong_collision_index]]; - let electronic_stopping_powers = material.electronic_stopping_cross_sections(particle_1, options.electronic_stopping_mode); - let n = material.number_densities(x, y, z); - - let delta_energy = match options.electronic_stopping_mode { - ElectronicStoppingMode::INTERPOLATED => electronic_stopping_powers.iter().zip(n).map(|(se, number_density)| se*number_density).collect::>().iter().sum::()*distance_traveled, - ElectronicStoppingMode::LOW_ENERGY_NONLOCAL => electronic_stopping_powers.iter().zip(n).map(|(se, number_density)| se*number_density).collect::>().iter().sum::()*distance_traveled, - ElectronicStoppingMode::LOW_ENERGY_LOCAL => oen_robinson_loss(particle_1.Z, strong_collision_Z, electronic_stopping_powers[strong_collision_index], x0, interaction_potential), - ElectronicStoppingMode::LOW_ENERGY_EQUIPARTITION => { - - let delta_energy_local = oen_robinson_loss(particle_1.Z, strong_collision_Z, electronic_stopping_powers[strong_collision_index], x0, interaction_potential); - let delta_energy_nonlocal = electronic_stopping_powers.iter().zip(n).map(|(se, number_density)| se*number_density).collect::>().iter().sum::()*distance_traveled; - - 0.5*delta_energy_local + 0.5*delta_energy_nonlocal - }, - }; - - particle_1.E += -delta_energy; - //Make sure particle energy doesn't become negative again - assert!(!particle_1.E.is_nan(), "Numerical error: particle energy is NaN following electronic stopping."); - if particle_1.E < 0. { - particle_1.E = 0.; - } - - particle_1.energy_loss(&options, recoil_energy, delta_energy); - } else if recoil_energy > 0. { - particle_1.energy_loss(&options, recoil_energy, 0.); - } -} - -/// Calculate the binary collision result of two particles for a given binary collision geometry. If the calculation fails, return an Error. -pub fn calculate_binary_collision(particle_1: &particle::Particle, particle_2: &particle::Particle, binary_collision_geometry: &BinaryCollisionGeometry, options: &Options) -> Result { - let Za: f64 = particle_1.Z; - let Zb: f64 = particle_2.Z; - let Ma: f64 = particle_1.m; - let Mb: f64 = particle_2.m; - let E0: f64 = particle_1.E; - let mu: f64 = Mb/(Ma + Mb); - - let interaction_potential = options.interaction_potential[particle_1.interaction_index][particle_2.interaction_index]; - let scattering_integral = options.scattering_integral[particle_1.interaction_index][particle_2.interaction_index]; - - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - let x0 = distance_of_closest_approach(particle_1, particle_2, binary_collision_geometry, options); - - let theta: f64 = match scattering_integral { - ScatteringIntegral::MENDENHALL_WELLER => mendenhall_weller(Za, Zb, Ma, Mb, E0, binary_collision_geometry.impact_parameter, x0, interaction_potential), - ScatteringIntegral::GAUSS_MEHLER{n_points} => gauss_mehler(Za, Zb, Ma, Mb, E0, binary_collision_geometry.impact_parameter, x0, interaction_potential, n_points), - ScatteringIntegral::GAUSS_LEGENDRE => gauss_legendre(Za, Zb, Ma, Mb, E0, binary_collision_geometry.impact_parameter, x0, interaction_potential), - ScatteringIntegral::MAGIC => magic(Za, Zb, Ma, Mb, E0, binary_collision_geometry.impact_parameter, x0, interaction_potential), - }; - - if theta.is_nan() { - return Err(anyhow!("Numerical error: CoM deflection angle is NaN for {}. Check input parameters.", binary_collision_geometry)); - } - - //See Eckstein 1991 for details on center of mass and lab frame angles - let asymptotic_deflection = match interaction_potential { - InteractionPotential::COULOMB{..} => 0., - _ => x0*a*(theta/2.).sin() - }; - let psi = (theta.sin().atan2(Ma/Mb + theta.cos())).abs(); - let psi_recoil = (theta.sin().atan2(1. - theta.cos())).abs(); - let recoil_energy = 4.*(Ma*Mb)/(Ma + Mb).powi(2)*E0*(theta/2.).sin().powi(2); - - Ok(BinaryCollisionResult::new(theta, psi, psi_recoil, recoil_energy, asymptotic_deflection, x0)) -} - - -/// Mendenhall-Weller scattering integrand. -fn scattering_integral_mw(x: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { - //Function for scattering integral - see Mendenhall and Weller, 1991 & 2005 - return (1. - interactions::phi(x, interaction_potential)/x/reduced_energy - beta*beta/x/x).powf(-0.5); -} - -/// Gauss-Legendre scattering integrand. -fn scattering_function_gl(u: f64, impact_parameter: f64, r0: f64, relative_energy: f64, interaction_potential: &dyn Fn(f64) -> f64) -> Result { - let result = 4.*impact_parameter*u/(r0*(1. - interaction_potential(r0/(1. - u*u))/relative_energy - impact_parameter*impact_parameter*(1. - u*u).powi(2)/r0/r0).sqrt()); - - if result.is_nan() { - Err(anyhow!("Numerical error: Gauss-Legendre scattering integrand complex. Likely incorrect distance of closest approach Er = {}, r0 = {} A, p = {} A - check root-finder.", - relative_energy/EV, r0/ANGSTROM, impact_parameter/ANGSTROM)) - } else { - Ok(result) - } -} - -/// Gauss-Mehler scattering integrand. -fn scattering_function_gm(u: f64, impact_parameter: f64, r0: f64, relative_energy: f64, interaction_potential: &dyn Fn(f64) -> f64) -> Result { - let result = impact_parameter/r0/(1. - interaction_potential(r0/u)/relative_energy - (impact_parameter*u/r0).powi(2)).sqrt(); - - if result.is_nan() { - Err(anyhow!("Numerical error: Gauss-Mehler scattering integrand complex. Likely incorrect distance of closest approach Er = {} eV r0 = {} A, p = {} A - check root-finder.", - relative_energy/EV, r0/ANGSTROM, impact_parameter/ANGSTROM)) - } else { - Ok(result) - } -} - -/// Compute the scattering integral for a given relative energy, distance of closest approach `r0`, and interaction potential using a Gauss-Mehler, n-point quadrature. -fn scattering_integral_gauss_mehler(impact_parameter: f64, relative_energy: f64, r0: f64, interaction_potential: &dyn Fn(f64) -> f64, n_points: usize) -> f64 { - let x: Vec = (1..=n_points).map(|i| ((2.*i as f64 - 1.)/4./n_points as f64*PI).cos()).collect(); - let w: Vec = (1..=n_points).map(|i| PI/n_points as f64*((2.*i as f64 - 1.)/4./n_points as f64*PI).sin()).collect(); - - PI - x.iter().zip(w) - .map(|(&x, w)| w*scattering_function_gm(x, impact_parameter, r0, relative_energy, interaction_potential) - .with_context(|| format!("Numerical error: NaN in Gauss-Mehler scattering integral at x = {} with Er = {} eV and p = {} A.", x, relative_energy/EV, impact_parameter/ANGSTROM)) - .unwrap()).sum::() -} - -/// Compute the scattering integral for a given relative energy, distance of closest approach `r0`, and interaction potential using a Gauss-Legendre, 5-point quadrature. -fn scattering_integral_gauss_legendre(impact_parameter: f64, relative_energy: f64, r0: f64, interaction_potential: &dyn Fn(f64) -> f64) -> f64 { - let x: Vec = vec![0., -0.538469, 0.538469, -0.90618, 0.90618].iter().map(|x| x/2. + 1./2.).collect(); - let w: Vec = vec![0.568889, 0.478629, 0.478629, 0.236927, 0.236927].iter().map(|w| w/2.).collect(); - - PI - x.iter().zip(w) - .map(|(&x, w)| w*scattering_function_gl(x, impact_parameter, r0, relative_energy, interaction_potential) - .with_context(|| format!("Numerical error: NaN in Gauss-Legendre scattering integral at x = {} with Er = {} eV and p = {} A.", x, relative_energy/EV, impact_parameter/ANGSTROM)) - .unwrap()).sum::() -} - -#[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] -/// Computes the distance of closest approach of two particles with atomic numbers `Za`, `Zb` and masses `Ma`, `Mb` for an inverse-polynomial interaction potential (e.g., Lennard-Jones) for a given impact parameter and incident energy `E0`. -/// Slightly complex roots with an imaginary part smaller than `polynom_complex_threshold` are considered real roots. -pub fn polynomial_rootfinder(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, - interaction_potential: InteractionPotential, polynom_complex_threshold: f64) -> Result { - - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; - let relative_energy = E0*Mb/(Ma + Mb); - - let coefficients = interactions::polynomial_coefficients(relative_energy, impact_parameter, interaction_potential); - let roots = real_polynomial_roots(coefficients.clone(), polynom_complex_threshold).unwrap(); - let max_root = roots.iter().cloned().fold(f64::NAN, f64::max); - let inverse_transformed_root = interactions::inverse_transform(max_root, interaction_potential); - - if roots.is_empty() || inverse_transformed_root.is_nan() { - return Err(anyhow!("Numerical error: polynomial rootfinder failed to find root with coefficients {:?}", - coefficients)) - } else { - return Ok(inverse_transformed_root/a) - } -} - -#[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] -/// Computes the distance of closest approach of two particles with atomic numbers `Za`, `Zb` and masses `Ma`, `Mb` for an arbitrary interaction potential (e.g., Morse) for a given impact parameter and incident energy `E0` using the Chebyshev-Proxy Root-Finder method. -/// -/// # Args: -/// -/// `Za`, `Zb`, `Ma`, `Mb`: atomic numbers and masses of the two particles -/// `E0`: incident energy of particle a in the lab frame. -/// `impact_parameter`: the impact parameter between particles a and b. -/// `interaction_potentaial`: the interaction potential between particles a and b. -/// `n0`: initial degree of Chebyshev interpolants. -/// `nmax`: maximum degree of Chebyshev interpolants. -/// `epsilon`: absolute tolerance of Chebyshev interpolant. -/// `complex_threshold`: slightly-complex roots with an imaginary part below this value are considered real. -/// `far_from_zero`: if the distance of closest approach function, evaluated over an interval [a, b] on the Lobatto grid, is always greater than this value, it is assumed that there are no roots in the interval [a, b]. -/// `truncation_threshold`: trailing terms of the Chebyshev interpolant with coefficients smaller than this value are ignored. -/// `interval_limit`: if subdivision produces an interval smaller than this value, the root-finder will panic. -/// `derivative_free`: if false, use Newton's method to polish roots from the CPR. If true, use the secant method. -/// -/// # Returns -/// Returns the distance of closest approach or an error if the root-finder failed. -pub fn cpr_rootfinder(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, - interaction_potential: InteractionPotential, n0: usize, nmax: usize, epsilon: f64, - complex_threshold: f64, truncation_threshold: f64, far_from_zero: f64, - interval_limit: f64, derivative_free: bool) -> Result { - - //Lindhard screening length and reduced energy - let a = interactions::screening_length(Za, Zb, interaction_potential); - let reduced_energy = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; - let relative_energy = E0*Mb/(Ma + Mb); - let p = impact_parameter; - - let f = |r: f64| -> f64 {interactions::distance_of_closest_approach_function(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)}; - let g = |r: f64| -> f64 {interactions::distance_of_closest_approach_function_singularity_free(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)* - interactions::scaling_function(r, impact_parameter, interaction_potential)}; - - //Using upper bound const, ~10, construct upper bound as a plateau near 0 and linear increase away from that - //let upper_bound = f64::max(upper_bound_const*p, upper_bound_const*a); - let upper_bound = impact_parameter + interactions::crossing_point_doca(interaction_potential); - - let roots = match derivative_free { - true => find_roots_with_secant_polishing(&g, &f, 0., upper_bound, - n0, epsilon, nmax, complex_threshold, - truncation_threshold, interval_limit, far_from_zero), - - false => { - let df = |r: f64| -> f64 {interactions::diff_distance_of_closest_approach_function(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)}; - find_roots_with_newton_polishing(&g, &f, &df, 0., upper_bound, - n0, epsilon, nmax, complex_threshold, - truncation_threshold, interval_limit, far_from_zero) - } - }.with_context(|| format!("Numerical error: CPR Rootfinder failed to converge when calculating distance of closest approach for Er = {} eV p = {} A using {}.", - relative_energy/EV, impact_parameter/ANGSTROM, interaction_potential)) - .unwrap(); - - let max_root = roots.iter().cloned().fold(f64::NAN, f64::max)/a; - - if roots.is_empty() || max_root.is_nan() { - return Err(anyhow!("Numerical error: CPR rootfinder failed to find root. x0: {}, F(a): {}, F(b): {};", max_root, g(0.), g(upper_bound))); - } else { - return Ok(max_root); - } -} - -/// Use Newton's method to find the distance of closest approach between two particles a and b. -pub fn newton_rootfinder(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, - interaction_potential: InteractionPotential, max_iterations: usize, tolerance: f64) -> Result { - - //Lindhard screening length and reduced energy - let a = interactions::screening_length(Za, Zb, interaction_potential); - let reduced_energy = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; - let relative_energy = E0*Mb/(Ma + Mb); - let beta = impact_parameter/a; - - let f = |r: f64| -> f64 {interactions::distance_of_closest_approach_function(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)}; - let df = |r: f64| -> f64 {interactions::diff_distance_of_closest_approach_function(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)}; - - //Guess for large reduced energy from Mendenhall and Weller 1991 - //For small energies, use pure Newton-Raphson with arbitrary guess of 1 - let mut x0 = beta; - let mut xn: f64; - if reduced_energy > 5. { - let inv_er_2 = 0.5/reduced_energy; - x0 = inv_er_2 + (inv_er_2*inv_er_2 + beta*beta).sqrt(); - } - - //Newton-Raphson to determine distance of closest approach - let mut err: f64 = tolerance + 1.; - for k in 0..max_iterations { - xn = x0 - f(x0*a)/df(x0*a); - err = (xn - x0)*(xn - x0); - x0 = xn; - if err < tolerance { - return Ok(x0); - } - } - return Err(anyhow!("Numerical error: exceeded maximum number of Newton-Raphson iterations, {}. E: {}; x0: {}; Error: {}; Tolerance: {}", - max_iterations, E0, x0, err, tolerance)); -} - -/// Gauss-Mehler quadrature. -pub fn gauss_mehler(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, x0: f64, interaction_potential: InteractionPotential, n_points: usize) -> f64 { - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - let r0 = x0*a; - let V = |r| {interactions::interaction_potential(r, a, Za, Zb, interaction_potential)}; - let relative_energy = E0*Mb/(Ma + Mb); - scattering_integral_gauss_mehler(impact_parameter, relative_energy, r0, &V, n_points) -} - -/// Gauss-Legendre quadrature. -pub fn gauss_legendre(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, x0: f64, interaction_potential: InteractionPotential) -> f64 { - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - let r0 = x0*a; - let V = |r| {interactions::interaction_potential(r, a, Za, Zb, interaction_potential)}; - let relative_energy = E0*Mb/(Ma + Mb); - scattering_integral_gauss_legendre(impact_parameter, relative_energy, r0, &V) -} - -/// Ziegler's MAGIC algorithm for approximating the scattering integral. -pub fn magic(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, x0: f64, interaction_potential: InteractionPotential) -> f64 { - //MAGIC algorithm - //Since this is legacy code I don't think I will clean this up - let C_ = match interaction_potential { - InteractionPotential::MOLIERE => vec![ 0.6743, 0.009611, 0.005175, 6.314, 10.0 ], - InteractionPotential::KR_C => vec![ 0.7887, 0.01166, 00.006913, 17.16, 10.79 ], - InteractionPotential::ZBL => vec![ 0.99229, 0.011615, 0.0071222, 9.3066, 14.813 ], - InteractionPotential::TRIDYN => vec![1.0144, 0.235809, 0.126, 69350., 83550.], //Undocumented Tridyn constants - _ => panic!("Input error: unimplemented interaction potential {} for MAGIC algorithm. Use a screened Coulomb potential.", interaction_potential) - }; - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - let beta: f64 = impact_parameter/a; - let V0 = Za*Zb*Q*Q/4.0/PI/EPS0/a; - let relative_energy = E0*Mb/(Ma + Mb); - let reduced_energy = relative_energy/V0; - let r0 = a*x0; - let V = V0*a/r0*interactions::phi(x0, interaction_potential); - let dV = -V/r0 + V0/r0*interactions::dphi(x0, interaction_potential); - let rho = -2.0*(relative_energy - V)/dV; - let D = 2.0*(1.0 + C_[0]/reduced_energy.sqrt())*reduced_energy*beta.powf((C_[1] + reduced_energy.sqrt())/(C_[2] + reduced_energy.sqrt())); - let G = (C_[4] + reduced_energy)/(C_[3] + reduced_energy)*((1. + D*D).sqrt() - D); - let delta = D*G/(1.0 + G)*(x0 - beta); - let ctheta2 = (beta + rho/a + delta)/(x0 + rho/a); - 2.*((beta + rho/a + delta)/(x0 + rho/a)).acos() -} - -/// Mendenhall-Weller quadrature. -pub fn mendenhall_weller(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, x0: f64, interaction_potential: InteractionPotential) -> f64 { - //Lindhard screening length and reduced energy - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; - let beta: f64 = impact_parameter/a; - - //Scattering integral quadrature from Mendenhall and Weller 2005 - let lambda0 = (0.5 + beta*beta/x0/x0/2. - interactions::dphi(x0, interaction_potential)/2./reduced_energy).powf(-1./2.); - let alpha = 1./12.*(1. + lambda0 + 5.*(0.4206*scattering_integral_mw(x0/0.9072, beta, reduced_energy, interaction_potential) + 0.9072*scattering_integral_mw(x0/0.4206, beta, reduced_energy, interaction_potential))); - PI*(1. - beta*alpha/x0) -} - -/// Oen-Robinson local electronic energy loss for a collision between particles a and b. -fn oen_robinson_loss(Za: f64, Zb: f64, Se: f64, x0: f64, interaction_potential: InteractionPotential) -> f64 { - //Oen-Robinson local electronic stopping power - let a = interactions::screening_length(Za, Zb, interaction_potential); - - //d1 is the first (smallest) interior constant of the screening function - let d1 = interactions::first_screening_radius(interaction_potential); - d1*d1/2./PI*Se*(-d1*x0).exp()/a/a -} +use super::*; + +#[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] +use rcpr::chebyshev::*; + +/// Geometrical quantities of binary collision. +pub struct BinaryCollisionGeometry { + pub phi_azimuthal: f64, + pub impact_parameter: f64, + pub mfp: f64 +} + +impl BinaryCollisionGeometry { + /// Constructs a new binary collision geometry object. + pub fn new(phi_azimuthal: f64, impact_parameter: f64, mfp: f64) -> BinaryCollisionGeometry { + BinaryCollisionGeometry { + phi_azimuthal, + impact_parameter, + mfp + } + } +} + +impl fmt::Display for BinaryCollisionGeometry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Binary collision geometry. \n phi_azimuthal = {} \n p = {} \n mfp = {} \n", + self.phi_azimuthal, self.impact_parameter, self.mfp) + } +} + +/// Resultant quantities of a binary collision. +pub struct BinaryCollisionResult { + pub theta: f64, + pub psi: f64, + pub psi_recoil: f64, + pub recoil_energy: f64, + pub asymptotic_deflection: f64, + pub normalized_distance_of_closest_approach: f64 +} + +impl BinaryCollisionResult { + + /// Constructs a new binary collision result object. + pub fn new(theta: f64, psi: f64, psi_recoil: f64, recoil_energy: f64, + asymptotic_deflection: f64, normalized_distance_of_closest_approach: f64) -> BinaryCollisionResult { + BinaryCollisionResult { + theta, + psi, + psi_recoil, + recoil_energy, + asymptotic_deflection, + normalized_distance_of_closest_approach + } + } +} + +impl fmt::Display for BinaryCollisionResult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Binary collision result. \n theta = {} \n psi = {} \n psi_r = {} \n E_recoil = {} \n tau = {} \n x0 = {} \n", + self.theta, self.psi, self.psi_recoil, self.recoil_energy, self.asymptotic_deflection, self.normalized_distance_of_closest_approach) + } +} + +/// This function takes a single particle, a material, and an options object and runs a binary-collision-approximation trajectory for that particle in that material, producing a final particle list that consists of the original ion and any material particles displaced thereby. +pub fn single_ion_bca(particle: particle::Particle, material: &material::Material, options: &Options) -> Vec { + + let mut particles: Vec = Vec::new(); + particles.push(particle); + + let mut particle_output: Vec = Vec::new(); + + let mut particle_index = particles.len(); + + while particle_index > 0 { + + //Remove particle from top of vector as particle_1 + let mut particle_1 = particles.pop().unwrap(); + + //println!("Particle start Z: {} E: {} ({}, {}, {})", particle_1.Z, particle_1.E/Q, particle_1.pos.x/ANGSTROM, particle_1.pos.y/ANGSTROM, particle_1.pos.z/ANGSTROM); + + //BCA loop + while !particle_1.stopped & !particle_1.left { + + //Choose impact parameters and azimuthal angles for all collisions, and determine mean free path + let binary_collision_geometries = bca::determine_mfp_phi_impact_parameter(&mut particle_1, &material, &options); + + #[cfg(feature = "accelerated_ions")] + let distance_to_target = if !material.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z) { + let (x, y, z) = material.geometry.closest_point(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + ((x - particle_1.pos.x).powi(2) + (y - particle_1.pos.y).powi(2) + (z - particle_1.pos.z).powi(2)).sqrt() + } else { + 0. + }; + + let mut total_energy_loss = 0.; + let mut total_asymptotic_deflection = 0.; + let mut normalized_distance_of_closest_approach = 0.; + let mut strong_collision_Z = 0.; + let mut strong_collision_index: usize = 0; + + //Collision loop + for (k, binary_collision_geometry) in binary_collision_geometries.iter().enumerate().take(options.weak_collision_order + 1) { + + let (species_index, mut particle_2) = bca::choose_collision_partner(&particle_1, &material, + &binary_collision_geometry, &options); + + //If recoil location is inside, proceed with binary collision loop + if material.inside(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z) & material.inside_energy_barrier(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z) { + + //Determine scattering angle from binary collision + let binary_collision_result = bca::calculate_binary_collision(&particle_1, + &particle_2, &binary_collision_geometry, &options) + .with_context(|| format!("Numerical error: binary collision calculation failed at x = {} y = {} with {}", + particle_1.pos.x, particle_2.pos.x, &binary_collision_geometry)) + .unwrap(); + + //println!("{}", binary_collision_result); + + //Only use 0th order collision for local electronic stopping + if k == 0 { + normalized_distance_of_closest_approach = binary_collision_result.normalized_distance_of_closest_approach; + strong_collision_Z = particle_2.Z; + strong_collision_index = species_index; + } + + //Energy transfer to recoil + particle_2.E = binary_collision_result.recoil_energy - material.average_bulk_binding_energy(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z); + particle_2.energy_origin = particle_2.E; + + //Accumulate asymptotic deflections for primary particle + total_energy_loss += binary_collision_result.recoil_energy; + + //total_deflection_angle += psi; + total_asymptotic_deflection += binary_collision_result.asymptotic_deflection; + + //Rotate particle 1, 2 by lab frame scattering angles + particle::rotate_particle(&mut particle_1, binary_collision_result.psi, + binary_collision_geometry.phi_azimuthal); + + particle::rotate_particle(&mut particle_2, -binary_collision_result.psi_recoil, + binary_collision_geometry.phi_azimuthal); + + particle_2.dir_old.x = particle_2.dir.x; + particle_2.dir_old.y = particle_2.dir.y; + particle_2.dir_old.z = particle_2.dir.z; + + //Only track number of strong collisions, i.e., k = 0 + if (binary_collision_result.psi > 0.) & (k == 0) { + particle_1.number_collision_events += 1; + } + + //Deep recoil suppression + //See Eckstein 1991 7.5.3 for recoil suppression function + if options.track_recoils & options.suppress_deep_recoils { + let E = particle_1.E; + let Za: f64 = particle_1.Z; + let Zb: f64 = particle_2.Z; + + let Ma: f64 = particle_1.m; + let Mb: f64 = particle_2.m; + + let n = material.total_number_density(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z); + //We just need the lindhard screening length here, so the particular potential is not important + let a: f64 = interactions::screening_length(Za, Zb, InteractionPotential::MOLIERE); + let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E; + let estimated_range_of_recoils = (reduced_energy.powf(0.3) + 0.1).powi(3)/n/a/a; + + let (x2, y2, z2) = material.closest_point(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z); + let dx = x2 - particle_2.pos.x; + let dy = y2 - particle_2.pos.y; + let dz = z2 - particle_2.pos.z; + let distance_to_surface = (dx*dx + dy*dy + dz*dz).sqrt(); + + if (distance_to_surface < estimated_range_of_recoils) & (particle_2.E > particle_2.Ec) { + particles.push(particle_2); + } + + //If transferred energy > cutoff energy, add recoil to particle vector + } else if options.track_recoils & (particle_2.E > particle_2.Ec) { + particles.push(particle_2); + } + } + } + + //Advance particle in space and track total distance traveled + #[cfg(not(feature = "accelerated_ions"))] + let distance_traveled = particle::particle_advance(&mut particle_1, + binary_collision_geometries[0].mfp, total_asymptotic_deflection); + + #[cfg(feature = "accelerated_ions")] + let distance_traveled = particle::particle_advance(&mut particle_1, + binary_collision_geometries[0].mfp + distance_to_target, total_asymptotic_deflection); + + //Subtract total energy from all simultaneous collisions and electronic stopping + bca::update_particle_energy(&mut particle_1, &material, distance_traveled, + total_energy_loss, normalized_distance_of_closest_approach, strong_collision_Z, + strong_collision_index, &options); + //println!("Particle finished collision loop Z: {} E: {} ({}, {}, {})", particle_1.Z, particle_1.E/Q, particle_1.pos.x/ANGSTROM, particle_1.pos.y/ANGSTROM, particle_1.pos.z/ANGSTROM); + + //println!("{} {} {}", energy_0/EV, energy_1/EV, (energy_1 - energy_0)/EV); + + //Check boundary conditions on leaving and stopping + material::boundary_condition_planar(&mut particle_1, &material); + + //Set particle index to topmost particle + particle_index = particles.len(); + } + //println!("Particle stopped or left Z: {} E: {} ({}, {}, {})", particle_1.Z, particle_1.E/Q, particle_1.pos.x/ANGSTROM, particle_1.pos.y/ANGSTROM, particle_1.pos.z/ANGSTROM); + particle_output.push(particle_1); + } + particle_output +} + +/// For a particle in a material, determine the mean free path and choose the azimuthal angle and impact parameter. +/// The mean free path can be exponentially distributed (gaseous) or constant (amorphous solid/liquid). Azimuthal angles are chosen uniformly. Impact parameters are chosen for collision partners distributed uniformly on a disk of density-dependent radius. +pub fn determine_mfp_phi_impact_parameter(particle_1: &mut particle::Particle, material: &material::Material, options: &Options) -> Vec { + + let x = particle_1.pos.x; + let y = particle_1.pos.y; + let z = particle_1.pos.z; + + let mut mfp = material.mfp(x, y, z); + + let mut phis_azimuthal = Vec::with_capacity(options.weak_collision_order + 1); + let mut binary_collision_geometries = Vec::with_capacity(options.weak_collision_order + 1); + + //Each weak collision gets its own aziumuthal angle in annuli around collision point + for k in 0..options.weak_collision_order + 1 { + phis_azimuthal.push(2.*PI*rand::random::()); + } + + if options.high_energy_free_flight_paths { + + let Ma: f64 = particle_1.m; + let Mb: f64 = material.average_mass(x, y, z); + let Za: f64 = particle_1.Z; + let Zb: f64 = material.average_Z(x, y, z); + let n: &Vec = material.number_densities(x, y, z); + let ck: f64 = material.electronic_stopping_correction_factor(x, y, z); + let E: f64 = particle_1.E; + let Ec: f64 = particle_1.Ec; + //We just need the Lindhard screening length here, so the particular potential is not important + let a: f64 = interactions::screening_length(Za, Zb, InteractionPotential::MOLIERE); + let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E; + + //Minimum energy transfer for generating scattering event set to cutoff energy + let E_min = Ec*(Ma + Mb)*(Ma + Mb)/4./Ma/Mb; + let reduced_energy_min: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E_min; + + //Free flight path formulation here described in SRIM textbook chapter 7, and Eckstein 1991 7.5.2 + let ep = (reduced_energy*reduced_energy_min).sqrt(); + let mut pmax = a/(ep + ep.sqrt() + 0.125*ep.powf(0.1)); + let mut ffp = 1./(material.total_number_density(x, y, z)*pmax*pmax*PI); + let stopping_powers = material.electronic_stopping_cross_sections(particle_1, ElectronicStoppingMode::INTERPOLATED); + + let delta_energy_electronic = stopping_powers.iter().zip(material.number_densities(x, y, z)).map(|(&se, number_density)| se*number_density).sum::()*ffp*ck; + + //If losing too much energy, scale free-flight-path down + //5 percent limit set in original TRIM paper, Biersack and Haggmark 1980 + if delta_energy_electronic > 0.05*E { + ffp *= 0.05*E/delta_energy_electronic; + pmax = (1./(material.total_number_density(x, y, z)*PI*ffp)).sqrt() + } + + //If free-flight-path less than the interatomic spacing, revert to amorphous solid model + //Mentioned in Eckstein 1991, Ziegler, Biersack, and Ziegler 2008 (SRIM textbook 7-8) + if ffp < mfp { + + ffp = mfp; + //Cylindrical geometry + pmax = mfp/SQRT2PI; + let mut impact_parameter = Vec::with_capacity(1); + let random_number = rand::random::(); + let p = pmax*random_number.sqrt(); + impact_parameter.push(p); + + //Atomically rough surface - scatter initial collisions using mfp near interface + if particle_1.first_step { + ffp = mfp*rand::random::(); + particle_1.first_step = false; + } + + ffp *= match options.mean_free_path_model { + MeanFreePathModel::GASEOUS => -rand::random::().ln(), + MeanFreePathModel::LIQUID => 1.0, + MeanFreePathModel::THRESHOLD{density} => { + if material.geometry.get_densities(x, y, z).iter().sum::() < density { + -rand::random::().ln() + } else { + 1.0 + } + } + }; + + binary_collision_geometries.push(BinaryCollisionGeometry::new(phis_azimuthal[0], impact_parameter[0], ffp)); + return binary_collision_geometries; + + } else { + + //Impact parameter chosen as sqrt(-ln(R))*pmax, as in Biersack and Haggmark 1980, + //And Mendenhall Weller 2005 + //And SRIM textbook chapter 7 + //And Eckstein 1991 + let mut impact_parameter = Vec::with_capacity(1); + let random_number = rand::random::(); + let p = pmax*(-random_number.ln()).sqrt(); + impact_parameter.push(p); + + //Atomically rough surface - scatter initial collisions using mfp near interface + if particle_1.first_step { + ffp = mfp*rand::random::(); + particle_1.first_step = false; + } + + if options.mean_free_path_model == MeanFreePathModel::GASEOUS { + ffp *= -rand::random::().ln(); + } + + binary_collision_geometries.push(BinaryCollisionGeometry::new(phis_azimuthal[0], impact_parameter[0], ffp)); + return binary_collision_geometries; + } + + } else { + + //If not using free flight paths, use weak collision model + let pmax = mfp/SQRTPI; + + //Cylindrical geometry + let mut impact_parameters = Vec::with_capacity(options.weak_collision_order + 1); + for k in 0..(options.weak_collision_order + 1) { + let random_number = rand::random::(); + let p = pmax*(random_number + k as f64).sqrt(); + impact_parameters.push(p) + } + + //Atomically rough surface - scatter initial collisions + if particle_1.first_step { + mfp *= rand::random::(); + particle_1.first_step = false; + } + + if options.mean_free_path_model == MeanFreePathModel::GASEOUS { + mfp *= -rand::random::().ln(); + } + + + for k in 0..(options.weak_collision_order + 1) { + binary_collision_geometries.push(BinaryCollisionGeometry::new(phis_azimuthal[k], impact_parameters[k], mfp)) + } + + return binary_collision_geometries; + } +} + +/// For a particle in a material, and for a particular binary collision geometry, choose a species for the collision partner. +pub fn choose_collision_partner(particle_1: &particle::Particle, material: &material::Material, binary_collision_geometry: &BinaryCollisionGeometry, options: &Options) -> (usize, particle::Particle) { + let x = particle_1.pos.x; + let y = particle_1.pos.y; + let z = particle_1.pos.z; + + let impact_parameter = binary_collision_geometry.impact_parameter; + let mfp = binary_collision_geometry.mfp; + let phi_azimuthal = binary_collision_geometry.phi_azimuthal; + + //Determine cosines and sines + let sinphi: f64 = phi_azimuthal.sin(); + let cosx: f64 = particle_1.dir.x; + let cosy: f64 = particle_1.dir.y; + let cosz: f64 = particle_1.dir.z; + let sinx: f64 = (1. - cosx*cosx).sqrt(); + let cosphi: f64 = phi_azimuthal.cos(); + + //Find recoil location + let x_recoil: f64 = x + mfp*cosx - impact_parameter*cosphi*sinx; + let y_recoil: f64 = y + mfp*cosy - impact_parameter*(sinphi*cosz - cosphi*cosy*cosx)/sinx; + let z_recoil: f64 = z + mfp*cosz + impact_parameter*(sinphi*cosy - cosphi*cosx*cosz)/sinx; + + //Choose recoil Z, M + let (species_index, Z_recoil, M_recoil, Ec_recoil, Es_recoil, interaction_index) = material.choose(x_recoil, y_recoil, z_recoil); + + return (species_index, + particle::Particle::new( + M_recoil, Z_recoil, 0., Ec_recoil, Es_recoil, + x_recoil, y_recoil, z_recoil, + cosx, cosy, cosz, + false, options.track_recoil_trajectories, interaction_index + ) + ) +} + +/// Calculate the distance of closest approach of two particles given a particular binary collision geometry. +fn distance_of_closest_approach(particle_1: &particle::Particle, particle_2: &particle::Particle, binary_collision_geometry: &BinaryCollisionGeometry, options: &Options) -> f64 { + let Za: f64 = particle_1.Z; + let Zb: f64 = particle_2.Z; + let Ma: f64 = particle_1.m; + let Mb: f64 = particle_2.m; + let E0: f64 = particle_1.E; + let relative_energy = E0*Mb/(Ma + Mb); + let p = binary_collision_geometry.impact_parameter; + + let interaction_potential = options.interaction_potential[particle_1.interaction_index][particle_2.interaction_index]; + + if let InteractionPotential::COULOMB{Za: Z1, Zb: Z2} = interaction_potential { + let doca = Z1*Z2*Q*Q/relative_energy/PI/EPS0/8. + (64.*(relative_energy*PI*p*EPS0).powi(2) + (Z1*Z2*Q*Q).powi(2)).sqrt()/relative_energy/PI/EPS0/8.; + return doca/interactions::screening_length(Z1, Z2, interaction_potential); + } + + let root_finder = if relative_energy < interactions::energy_threshold_single_root(interaction_potential) { + options.root_finder[particle_1.interaction_index][particle_2.interaction_index] + } else {Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-6}}; + + #[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] + match root_finder { + Rootfinder::POLYNOMIAL{complex_threshold} => polynomial_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, complex_threshold) + .with_context(|| format!("Numerical error: Polynomial rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) + .unwrap(), + Rootfinder::CPR{n0, nmax, epsilon, complex_threshold, truncation_threshold, far_from_zero, interval_limit, derivative_free} => + cpr_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, n0, nmax, epsilon, complex_threshold, truncation_threshold, far_from_zero, interval_limit, derivative_free) + .with_context(|| format!("Numerical error: CPR rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) + .unwrap(), + Rootfinder::NEWTON{max_iterations, tolerance} => newton_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, max_iterations, tolerance) + .with_context(|| format!("Numerical error: Newton rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) + .unwrap(), + Rootfinder::DEFAULTNEWTON => newton_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, 100, 1E-3) + .with_context(|| format!("Numerical error: Newton rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) + .unwrap(), + } + + #[cfg(not(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl")))] + match root_finder { + Rootfinder::NEWTON{max_iterations, tolerance} => newton_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, max_iterations, tolerance) + .with_context(|| format!("Numerical error: Newton rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) + .unwrap(), + Rootfinder::DEFAULTNEWTON => newton_rootfinder(Za, Zb, Ma, Mb, E0, p, interaction_potential, 100, 1E-3) + .with_context(|| format!("Numerical error: Newton rootfinder failed for {} at {} eV with p = {} A.", interaction_potential, E0/EV, p/ANGSTROM)) + .unwrap(), + _ => panic!("Input error: unimplemented root-finder. Choose NEWTON or build with cpr_rootfinder to enable CPR and POLYNOMIAL") + } +} + +/// Update a particle's energy following nuclear and electronic interactions of a single BCA step. +pub fn update_particle_energy(particle_1: &mut particle::Particle, material: &material::Material, distance_traveled: f64, + recoil_energy: f64, x0: f64, strong_collision_Z: f64, strong_collision_index: usize, options: &Options) { + + //If particle energy drops below zero before electronic stopping calcualtion, it produces NaNs + particle_1.E -= recoil_energy; + assert!(!particle_1.E.is_nan(), "Numerical error: particle energy is NaN following collision."); + if particle_1.E < 0. { + particle_1.E = 0.; + } + + let x = particle_1.pos.x; + let y = particle_1.pos.y; + let z = particle_1.pos.z; + + if material.inside(x, y, z) { + + let interaction_potential = options.interaction_potential[particle_1.interaction_index][material.interaction_index[strong_collision_index]]; + let electronic_stopping_powers = material.electronic_stopping_cross_sections(particle_1, options.electronic_stopping_mode); + let n = material.number_densities(x, y, z); + + let delta_energy = match options.electronic_stopping_mode { + ElectronicStoppingMode::INTERPOLATED => electronic_stopping_powers.iter().zip(n).map(|(se, number_density)| se*number_density).collect::>().iter().sum::()*distance_traveled, + ElectronicStoppingMode::LOW_ENERGY_NONLOCAL => electronic_stopping_powers.iter().zip(n).map(|(se, number_density)| se*number_density).collect::>().iter().sum::()*distance_traveled, + ElectronicStoppingMode::LOW_ENERGY_LOCAL => oen_robinson_loss(particle_1.Z, strong_collision_Z, electronic_stopping_powers[strong_collision_index], x0, interaction_potential), + ElectronicStoppingMode::LOW_ENERGY_EQUIPARTITION => { + + let delta_energy_local = oen_robinson_loss(particle_1.Z, strong_collision_Z, electronic_stopping_powers[strong_collision_index], x0, interaction_potential); + let delta_energy_nonlocal = electronic_stopping_powers.iter().zip(n).map(|(se, number_density)| se*number_density).collect::>().iter().sum::()*distance_traveled; + + 0.5*delta_energy_local + 0.5*delta_energy_nonlocal + }, + }; + + particle_1.E += -delta_energy; + //Make sure particle energy doesn't become negative again + assert!(!particle_1.E.is_nan(), "Numerical error: particle energy is NaN following electronic stopping."); + if particle_1.E < 0. { + particle_1.E = 0.; + } + + particle_1.energy_loss(&options, recoil_energy, delta_energy); + } else if recoil_energy > 0. { + particle_1.energy_loss(&options, recoil_energy, 0.); + } +} + +/// Calculate the binary collision result of two particles for a given binary collision geometry. If the calculation fails, return an Error. +pub fn calculate_binary_collision(particle_1: &particle::Particle, particle_2: &particle::Particle, binary_collision_geometry: &BinaryCollisionGeometry, options: &Options) -> Result { + let Za: f64 = particle_1.Z; + let Zb: f64 = particle_2.Z; + let Ma: f64 = particle_1.m; + let Mb: f64 = particle_2.m; + let E0: f64 = particle_1.E; + let mu: f64 = Mb/(Ma + Mb); + + let interaction_potential = options.interaction_potential[particle_1.interaction_index][particle_2.interaction_index]; + let scattering_integral = options.scattering_integral[particle_1.interaction_index][particle_2.interaction_index]; + + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + let x0 = distance_of_closest_approach(particle_1, particle_2, binary_collision_geometry, options); + + let theta: f64 = match scattering_integral { + ScatteringIntegral::MENDENHALL_WELLER => mendenhall_weller(Za, Zb, Ma, Mb, E0, binary_collision_geometry.impact_parameter, x0, interaction_potential), + ScatteringIntegral::GAUSS_MEHLER{n_points} => gauss_mehler(Za, Zb, Ma, Mb, E0, binary_collision_geometry.impact_parameter, x0, interaction_potential, n_points), + ScatteringIntegral::GAUSS_LEGENDRE => gauss_legendre(Za, Zb, Ma, Mb, E0, binary_collision_geometry.impact_parameter, x0, interaction_potential), + ScatteringIntegral::MAGIC => magic(Za, Zb, Ma, Mb, E0, binary_collision_geometry.impact_parameter, x0, interaction_potential), + }; + + if theta.is_nan() { + return Err(anyhow!("Numerical error: CoM deflection angle is NaN for {}. Check input parameters.", binary_collision_geometry)); + } + + //See Eckstein 1991 for details on center of mass and lab frame angles + let asymptotic_deflection = match interaction_potential { + InteractionPotential::COULOMB{..} => 0., + _ => x0*a*(theta/2.).sin() + }; + let psi = (theta.sin().atan2(Ma/Mb + theta.cos())).abs(); + let psi_recoil = (theta.sin().atan2(1. - theta.cos())).abs(); + let recoil_energy = 4.*(Ma*Mb)/(Ma + Mb).powi(2)*E0*(theta/2.).sin().powi(2); + + Ok(BinaryCollisionResult::new(theta, psi, psi_recoil, recoil_energy, asymptotic_deflection, x0)) +} + + +/// Mendenhall-Weller scattering integrand. +fn scattering_integral_mw(x: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { + //Function for scattering integral - see Mendenhall and Weller, 1991 & 2005 + return (1. - interactions::phi(x, interaction_potential)/x/reduced_energy - beta*beta/x/x).powf(-0.5); +} + +/// Gauss-Legendre scattering integrand. +fn scattering_function_gl(u: f64, impact_parameter: f64, r0: f64, relative_energy: f64, interaction_potential: &dyn Fn(f64) -> f64) -> Result { + let result = 4.*impact_parameter*u/(r0*(1. - interaction_potential(r0/(1. - u*u))/relative_energy - impact_parameter*impact_parameter*(1. - u*u).powi(2)/r0/r0).sqrt()); + + if result.is_nan() { + Err(anyhow!("Numerical error: Gauss-Legendre scattering integrand complex. Likely incorrect distance of closest approach Er = {}, r0 = {} A, p = {} A - check root-finder.", + relative_energy/EV, r0/ANGSTROM, impact_parameter/ANGSTROM)) + } else { + Ok(result) + } +} + +/// Gauss-Mehler scattering integrand. +fn scattering_function_gm(u: f64, impact_parameter: f64, r0: f64, relative_energy: f64, interaction_potential: &dyn Fn(f64) -> f64) -> Result { + let result = impact_parameter/r0/(1. - interaction_potential(r0/u)/relative_energy - (impact_parameter*u/r0).powi(2)).sqrt(); + + if result.is_nan() { + Err(anyhow!("Numerical error: Gauss-Mehler scattering integrand complex. Likely incorrect distance of closest approach Er = {} eV r0 = {} A, p = {} A - check root-finder.", + relative_energy/EV, r0/ANGSTROM, impact_parameter/ANGSTROM)) + } else { + Ok(result) + } +} + +/// Compute the scattering integral for a given relative energy, distance of closest approach `r0`, and interaction potential using a Gauss-Mehler, n-point quadrature. +fn scattering_integral_gauss_mehler(impact_parameter: f64, relative_energy: f64, r0: f64, interaction_potential: &dyn Fn(f64) -> f64, n_points: usize) -> f64 { + let x: Vec = (1..=n_points).map(|i| ((2.*i as f64 - 1.)/4./n_points as f64*PI).cos()).collect(); + let w: Vec = (1..=n_points).map(|i| PI/n_points as f64*((2.*i as f64 - 1.)/4./n_points as f64*PI).sin()).collect(); + + PI - x.iter().zip(w) + .map(|(&x, w)| w*scattering_function_gm(x, impact_parameter, r0, relative_energy, interaction_potential) + .with_context(|| format!("Numerical error: NaN in Gauss-Mehler scattering integral at x = {} with Er = {} eV and p = {} A.", x, relative_energy/EV, impact_parameter/ANGSTROM)) + .unwrap()).sum::() +} + +/// Compute the scattering integral for a given relative energy, distance of closest approach `r0`, and interaction potential using a Gauss-Legendre, 5-point quadrature. +fn scattering_integral_gauss_legendre(impact_parameter: f64, relative_energy: f64, r0: f64, interaction_potential: &dyn Fn(f64) -> f64) -> f64 { + let x: Vec = vec![0., -0.538469, 0.538469, -0.90618, 0.90618].iter().map(|x| x/2. + 1./2.).collect(); + let w: Vec = vec![0.568889, 0.478629, 0.478629, 0.236927, 0.236927].iter().map(|w| w/2.).collect(); + + PI - x.iter().zip(w) + .map(|(&x, w)| w*scattering_function_gl(x, impact_parameter, r0, relative_energy, interaction_potential) + .with_context(|| format!("Numerical error: NaN in Gauss-Legendre scattering integral at x = {} with Er = {} eV and p = {} A.", x, relative_energy/EV, impact_parameter/ANGSTROM)) + .unwrap()).sum::() +} + +#[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] +/// Computes the distance of closest approach of two particles with atomic numbers `Za`, `Zb` and masses `Ma`, `Mb` for an inverse-polynomial interaction potential (e.g., Lennard-Jones) for a given impact parameter and incident energy `E0`. +/// Slightly complex roots with an imaginary part smaller than `polynom_complex_threshold` are considered real roots. +pub fn polynomial_rootfinder(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, + interaction_potential: InteractionPotential, polynom_complex_threshold: f64) -> Result { + + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; + let relative_energy = E0*Mb/(Ma + Mb); + + let coefficients = interactions::polynomial_coefficients(relative_energy, impact_parameter, interaction_potential); + let roots = real_polynomial_roots(coefficients.clone(), polynom_complex_threshold).unwrap(); + let max_root = roots.iter().cloned().fold(f64::NAN, f64::max); + let inverse_transformed_root = interactions::inverse_transform(max_root, interaction_potential); + + if roots.is_empty() || inverse_transformed_root.is_nan() { + return Err(anyhow!("Numerical error: polynomial rootfinder failed to find root with coefficients {:?}", + coefficients)) + } else { + return Ok(inverse_transformed_root/a) + } +} + +#[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] +/// Computes the distance of closest approach of two particles with atomic numbers `Za`, `Zb` and masses `Ma`, `Mb` for an arbitrary interaction potential (e.g., Morse) for a given impact parameter and incident energy `E0` using the Chebyshev-Proxy Root-Finder method. +/// +/// # Args: +/// +/// `Za`, `Zb`, `Ma`, `Mb`: atomic numbers and masses of the two particles +/// `E0`: incident energy of particle a in the lab frame. +/// `impact_parameter`: the impact parameter between particles a and b. +/// `interaction_potentaial`: the interaction potential between particles a and b. +/// `n0`: initial degree of Chebyshev interpolants. +/// `nmax`: maximum degree of Chebyshev interpolants. +/// `epsilon`: absolute tolerance of Chebyshev interpolant. +/// `complex_threshold`: slightly-complex roots with an imaginary part below this value are considered real. +/// `far_from_zero`: if the distance of closest approach function, evaluated over an interval [a, b] on the Lobatto grid, is always greater than this value, it is assumed that there are no roots in the interval [a, b]. +/// `truncation_threshold`: trailing terms of the Chebyshev interpolant with coefficients smaller than this value are ignored. +/// `interval_limit`: if subdivision produces an interval smaller than this value, the root-finder will panic. +/// `derivative_free`: if false, use Newton's method to polish roots from the CPR. If true, use the secant method. +/// +/// # Returns +/// Returns the distance of closest approach or an error if the root-finder failed. +pub fn cpr_rootfinder(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, + interaction_potential: InteractionPotential, n0: usize, nmax: usize, epsilon: f64, + complex_threshold: f64, truncation_threshold: f64, far_from_zero: f64, + interval_limit: f64, derivative_free: bool) -> Result { + + //Lindhard screening length and reduced energy + let a = interactions::screening_length(Za, Zb, interaction_potential); + let reduced_energy = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; + let relative_energy = E0*Mb/(Ma + Mb); + let p = impact_parameter; + + let f = |r: f64| -> f64 {interactions::distance_of_closest_approach_function(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)}; + let g = |r: f64| -> f64 {interactions::distance_of_closest_approach_function_singularity_free(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)* + interactions::scaling_function(r, impact_parameter, interaction_potential)}; + + //Using upper bound const, ~10, construct upper bound as a plateau near 0 and linear increase away from that + //let upper_bound = f64::max(upper_bound_const*p, upper_bound_const*a); + let upper_bound = impact_parameter + interactions::crossing_point_doca(interaction_potential); + + let roots = match derivative_free { + true => find_roots_with_secant_polishing(&g, &f, 0., upper_bound, + n0, epsilon, nmax, complex_threshold, + truncation_threshold, interval_limit, far_from_zero), + + false => { + let df = |r: f64| -> f64 {interactions::diff_distance_of_closest_approach_function(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)}; + find_roots_with_newton_polishing(&g, &f, &df, 0., upper_bound, + n0, epsilon, nmax, complex_threshold, + truncation_threshold, interval_limit, far_from_zero) + } + }.with_context(|| format!("Numerical error: CPR Rootfinder failed to converge when calculating distance of closest approach for Er = {} eV p = {} A using {}.", + relative_energy/EV, impact_parameter/ANGSTROM, interaction_potential)) + .unwrap(); + + let max_root = roots.iter().cloned().fold(f64::NAN, f64::max)/a; + + if roots.is_empty() || max_root.is_nan() { + return Err(anyhow!("Numerical error: CPR rootfinder failed to find root. x0: {}, F(a): {}, F(b): {};", max_root, g(0.), g(upper_bound))); + } else { + return Ok(max_root); + } +} + +/// Use Newton's method to find the distance of closest approach between two particles a and b. +pub fn newton_rootfinder(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, + interaction_potential: InteractionPotential, max_iterations: usize, tolerance: f64) -> Result { + + //Lindhard screening length and reduced energy + let a = interactions::screening_length(Za, Zb, interaction_potential); + let reduced_energy = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; + let relative_energy = E0*Mb/(Ma + Mb); + let beta = impact_parameter/a; + + let f = |r: f64| -> f64 {interactions::distance_of_closest_approach_function(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)}; + let df = |r: f64| -> f64 {interactions::diff_distance_of_closest_approach_function(r, a, Za, Zb, relative_energy, impact_parameter, interaction_potential)}; + + //Guess for large reduced energy from Mendenhall and Weller 1991 + //For small energies, use pure Newton-Raphson with arbitrary guess of 1 + let mut x0 = beta; + let mut xn: f64; + if reduced_energy > 5. { + let inv_er_2 = 0.5/reduced_energy; + x0 = inv_er_2 + (inv_er_2*inv_er_2 + beta*beta).sqrt(); + } + + //Newton-Raphson to determine distance of closest approach + let mut err: f64 = tolerance + 1.; + for k in 0..max_iterations { + xn = x0 - f(x0*a)/df(x0*a); + err = (xn - x0)*(xn - x0); + x0 = xn; + if err < tolerance { + return Ok(x0); + } + } + return Err(anyhow!("Numerical error: exceeded maximum number of Newton-Raphson iterations, {}. E: {}; x0: {}; Error: {}; Tolerance: {}", + max_iterations, E0, x0, err, tolerance)); +} + +/// Gauss-Mehler quadrature. +pub fn gauss_mehler(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, x0: f64, interaction_potential: InteractionPotential, n_points: usize) -> f64 { + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + let r0 = x0*a; + let V = |r| {interactions::interaction_potential(r, a, Za, Zb, interaction_potential)}; + let relative_energy = E0*Mb/(Ma + Mb); + scattering_integral_gauss_mehler(impact_parameter, relative_energy, r0, &V, n_points) +} + +/// Gauss-Legendre quadrature. +pub fn gauss_legendre(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, x0: f64, interaction_potential: InteractionPotential) -> f64 { + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + let r0 = x0*a; + let V = |r| {interactions::interaction_potential(r, a, Za, Zb, interaction_potential)}; + let relative_energy = E0*Mb/(Ma + Mb); + scattering_integral_gauss_legendre(impact_parameter, relative_energy, r0, &V) +} + +/// Ziegler's MAGIC algorithm for approximating the scattering integral. +pub fn magic(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, x0: f64, interaction_potential: InteractionPotential) -> f64 { + //MAGIC algorithm + //Since this is legacy code I don't think I will clean this up + let C_ = match interaction_potential { + InteractionPotential::MOLIERE => vec![ 0.6743, 0.009611, 0.005175, 6.314, 10.0 ], + InteractionPotential::KR_C => vec![ 0.7887, 0.01166, 00.006913, 17.16, 10.79 ], + InteractionPotential::ZBL => vec![ 0.99229, 0.011615, 0.0071222, 9.3066, 14.813 ], + InteractionPotential::TRIDYN => vec![1.0144, 0.235809, 0.126, 69350., 83550.], //Undocumented Tridyn constants + _ => panic!("Input error: unimplemented interaction potential {} for MAGIC algorithm. Use a screened Coulomb potential.", interaction_potential) + }; + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + let beta: f64 = impact_parameter/a; + let V0 = Za*Zb*Q*Q/4.0/PI/EPS0/a; + let relative_energy = E0*Mb/(Ma + Mb); + let reduced_energy = relative_energy/V0; + let r0 = a*x0; + let V = V0*a/r0*interactions::phi(x0, interaction_potential); + let dV = -V/r0 + V0/r0*interactions::dphi(x0, interaction_potential); + let rho = -2.0*(relative_energy - V)/dV; + let D = 2.0*(1.0 + C_[0]/reduced_energy.sqrt())*reduced_energy*beta.powf((C_[1] + reduced_energy.sqrt())/(C_[2] + reduced_energy.sqrt())); + let G = (C_[4] + reduced_energy)/(C_[3] + reduced_energy)*((1. + D*D).sqrt() - D); + let delta = D*G/(1.0 + G)*(x0 - beta); + let ctheta2 = (beta + rho/a + delta)/(x0 + rho/a); + 2.*((beta + rho/a + delta)/(x0 + rho/a)).acos() +} + +/// Mendenhall-Weller quadrature. +pub fn mendenhall_weller(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, impact_parameter: f64, x0: f64, interaction_potential: InteractionPotential) -> f64 { + //Lindhard screening length and reduced energy + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; + let beta: f64 = impact_parameter/a; + + //Scattering integral quadrature from Mendenhall and Weller 2005 + let lambda0 = (0.5 + beta*beta/x0/x0/2. - interactions::dphi(x0, interaction_potential)/2./reduced_energy).powf(-1./2.); + let alpha = 1./12.*(1. + lambda0 + 5.*(0.4206*scattering_integral_mw(x0/0.9072, beta, reduced_energy, interaction_potential) + 0.9072*scattering_integral_mw(x0/0.4206, beta, reduced_energy, interaction_potential))); + PI*(1. - beta*alpha/x0) +} + +/// Oen-Robinson local electronic energy loss for a collision between particles a and b. +fn oen_robinson_loss(Za: f64, Zb: f64, Se: f64, x0: f64, interaction_potential: InteractionPotential) -> f64 { + //Oen-Robinson local electronic stopping power + let a = interactions::screening_length(Za, Zb, interaction_potential); + + //d1 is the first (smallest) interior constant of the screening function + let d1 = interactions::first_screening_radius(interaction_potential); + d1*d1/2./PI*Se*(-d1*x0).exp()/a/a +} diff --git a/src/consts.rs b/src/consts.rs index eb36cd5..a0dfcd2 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,35 +1,35 @@ -use super::*; - -//Physical constants -///Fundamental charge in Coulombs. -pub const Q: f64 = 1.602176634E-19; -/// One electron-volt in Joules. -pub const EV: f64 = Q; -/// One atomic mass unit in kilograms. -pub const AMU: f64 = 1.66053906660E-27; -/// One Angstrom in meters. -pub const ANGSTROM: f64 = 1E-10; -/// One micron in meters. -pub const MICRON: f64 = 1E-6; -/// One nanometer in meters. -pub const NM: f64 = 1E-9; -/// One centimeter in meters. -pub const CM: f64 = 1E-2; -/// Vacuum permitivity in Farads/meter. -pub const EPS0: f64 = 8.8541878128E-12; -/// Bohr radius in meters. -pub const A0: f64 = 5.29177210903E-11; -/// Electron mass in kilograms. -pub const ME: f64 = 9.1093837015E-31; -/// sqrt(pi). -pub const SQRTPI: f64 = 2. / FRAC_2_SQRT_PI; -/// sqrt(2 * pi). -pub const SQRT2PI: f64 = 2. * SQRT_2 / FRAC_2_SQRT_PI; -/// Speed of light in meters/second. -pub const C: f64 = 299792458.; -/// Bethe-Bloch electronic stopping prefactor, in SI units. -pub const BETHE_BLOCH_PREFACTOR: f64 = 4.*PI*(Q*Q/(4.*PI*EPS0))*(Q*Q/(4.*PI*EPS0))/ME/C/C; -/// Lindhard-Scharff electronic stopping prefactor, in SI units. -pub const LINDHARD_SCHARFF_PREFACTOR: f64 = 1.212*ANGSTROM*ANGSTROM*Q; -/// Lindhard reduced energy prefactor, in SI units. -pub const LINDHARD_REDUCED_ENERGY_PREFACTOR: f64 = 4.*PI*EPS0/Q/Q; +use super::*; + +//Physical constants +///Fundamental charge in Coulombs. +pub const Q: f64 = 1.602176634E-19; +/// One electron-volt in Joules. +pub const EV: f64 = Q; +/// One atomic mass unit in kilograms. +pub const AMU: f64 = 1.66053906660E-27; +/// One Angstrom in meters. +pub const ANGSTROM: f64 = 1E-10; +/// One micron in meters. +pub const MICRON: f64 = 1E-6; +/// One nanometer in meters. +pub const NM: f64 = 1E-9; +/// One centimeter in meters. +pub const CM: f64 = 1E-2; +/// Vacuum permitivity in Farads/meter. +pub const EPS0: f64 = 8.8541878128E-12; +/// Bohr radius in meters. +pub const A0: f64 = 5.29177210903E-11; +/// Electron mass in kilograms. +pub const ME: f64 = 9.1093837015E-31; +/// sqrt(pi). +pub const SQRTPI: f64 = 2. / FRAC_2_SQRT_PI; +/// sqrt(2 * pi). +pub const SQRT2PI: f64 = 2. * SQRT_2 / FRAC_2_SQRT_PI; +/// Speed of light in meters/second. +pub const C: f64 = 299792458.; +/// Bethe-Bloch electronic stopping prefactor, in SI units. +pub const BETHE_BLOCH_PREFACTOR: f64 = 4.*PI*(Q*Q/(4.*PI*EPS0))*(Q*Q/(4.*PI*EPS0))/ME/C/C; +/// Lindhard-Scharff electronic stopping prefactor, in SI units. +pub const LINDHARD_SCHARFF_PREFACTOR: f64 = 1.212*ANGSTROM*ANGSTROM*Q; +/// Lindhard reduced energy prefactor, in SI units. +pub const LINDHARD_REDUCED_ENERGY_PREFACTOR: f64 = 4.*PI*EPS0/Q/Q; diff --git a/src/enums.rs b/src/enums.rs index c3aa849..07a7cbd 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,246 +1,246 @@ -use super::*; - -pub enum MaterialType { - MESH0D(material::Material), - MESH1D(material::Material), - MESH2D(material::Material), - SPHERE(material::Material), - #[cfg(feature = "parry3d")] - BALL(material::Material), - #[cfg(feature = "parry3d")] - TRIMESH(material::Material) -} - -#[derive(Deserialize)] -pub enum GeometryType { - MESH0D, - MESH1D, - MESH2D, - SPHERE, - #[cfg(feature = "parry3d")] - BALL, - #[cfg(feature = "parry3d")] - TRIMESH, -} - -/// Mode of electronic stopping to use. -#[derive(Deserialize, PartialEq, Clone, Copy)] -pub enum ElectronicStoppingMode { - /// Biersack-Varelas interpolated electronic stopping. Valid for ~eV/nucleon to ~GeV/nucleon. - INTERPOLATED, - /// Oen-Robinson Firsov-type local electronic stopping. Valid up to ~25 keV/nucleon. - LOW_ENERGY_LOCAL, - /// Lindhard-Scharff nonlocal electronic stopping. Valid up to ~25 keV/nucleon. - LOW_ENERGY_NONLOCAL, - /// Equipartition between Oen-Robinson and Lindhard-Scharff electronic stopping formulas. - LOW_ENERGY_EQUIPARTITION, -} - -impl fmt::Display for ElectronicStoppingMode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ElectronicStoppingMode::INTERPOLATED => write!(f, "Biersack-Varelas electronic stopping"), - ElectronicStoppingMode::LOW_ENERGY_NONLOCAL => write!(f, "Lindhard-Scharff electronic stopping"), - ElectronicStoppingMode::LOW_ENERGY_LOCAL => write!(f, "Oen-Robinson electronic stopping"), - ElectronicStoppingMode::LOW_ENERGY_EQUIPARTITION => write!(f, "Equipartition with Lindhard-Scharff and Oen-Robinson"), - } - } -} - -/// Mode of surface binding energy calculation. -#[derive(Deserialize, PartialEq, Clone, Copy)] -pub enum SurfaceBindingCalculation { - /// Surface binding energies will be determined individually depending only on the particle's `Es`. - INDIVIDUAL, - /// Surface binding energies will be a concentration-weighted average of material surface-binding energies, unless `particle.Es == 0` in which case it will be zero. - TARGET, - /// Surface binding energies will be the average of the particle and TARGET, unless either is zero in which case it will be zero. - AVERAGE, -} - -impl fmt::Display for SurfaceBindingCalculation { - fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - SurfaceBindingCalculation::INDIVIDUAL => write!(f, - "Individual surface binding energies."), - SurfaceBindingCalculation::TARGET => write!(f, - "Concentration-dependent linear combinaion of target binding energies."), - SurfaceBindingCalculation::AVERAGE => write!(f, - "Average between particle and concentration-dependent linear combination of target binding energies."), - } - } -} - -#[derive(Deserialize, PartialEq, Clone, Copy)] -pub enum SurfaceBindingModel { - /// Isotropic surface binding potential - results in no refraction - ISOTROPIC{calculation: SurfaceBindingCalculation}, - /// Planar surface binding potential - particles refract through potential - PLANAR{calculation: SurfaceBindingCalculation}, - /// Planar surface binding potential default - particles refract through potential - TARGET, - INDIVIDUAL, - AVERAGE -} - -impl fmt::Display for SurfaceBindingModel { - fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - SurfaceBindingModel::ISOTROPIC{..} => write!(f, - "Locally isotropic surface binding energy."), - SurfaceBindingModel::PLANAR{..} => write!(f, - "Locally planar surface binding energy."), - SurfaceBindingModel::TARGET => write!(f, "Locally planar surface binding energy."), - SurfaceBindingModel::INDIVIDUAL =>write!(f, "Locally planar surface binding energy."), - SurfaceBindingModel::AVERAGE =>write!(f, "Locally planar surface binding energy."), - } - } -} - -/// Mode of bulk binding energy calculation. -#[derive(Deserialize, PartialEq, Clone, Copy)] -pub enum BulkBindingModel { - /// Bulk binding energies will be determined individually depending only on the particle's `Eb`. - INDIVIDUAL, - /// Bulk binding energies will be the concentration-weighted average, unless either is zero in which case it will be zero. - AVERAGE, -} - -impl fmt::Display for BulkBindingModel { - fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - BulkBindingModel::INDIVIDUAL => write!(f, - "Individual bulk binding energies."), - - BulkBindingModel::AVERAGE => write!(f, - "Concentration-weighted average bulk binding energy."), - } - } -} - -/// Mean-free-path model. -#[derive(Deserialize, PartialEq, Clone, Copy)] -pub enum MeanFreePathModel { - /// Constant mean-free-path for liquids and amorphous solids. - LIQUID, - /// Exponentially-distributed mean-free-paths for gases. - GASEOUS, - /// Switch from gas (below threshold) to liquid (above threshold). - THRESHOLD{density: f64} -} - -impl fmt::Display for MeanFreePathModel { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - MeanFreePathModel::LIQUID => write!(f, "Amorphous Solid/Liquid Model"), - MeanFreePathModel::GASEOUS => write!(f, "Gaseous Model"), - MeanFreePathModel::THRESHOLD{density} => write!(f, "Gaseous if n0 < {}, Liquid/Amorphous Solid otherwise.", density) - } - } -} - -/// Interatomic potentials between particles in rustbca. -#[derive(Deserialize, Clone, Copy)] -pub enum InteractionPotential { - /// TRIDYN-style Kr-C. Equivalent to KR_C, except for the MAGIC constants. - TRIDYN, - /// Moliere's approximation to the Thomas-Fermi interatomic potential. - MOLIERE, - /// Krypton-Carbon "universal" interatomic potential. - KR_C, - /// Ziegler-Biersack-Littmark "unviversal" semi-empirical interatomic potential. - ZBL, - /// Lenz-Jensen screened Coulomb potential. - LENZ_JENSEN, - /// Lennard-Jones 12-6 potential, with user-defined sigma and epsilon. - LENNARD_JONES_12_6 {sigma: f64, epsilon: f64}, - /// Lennard-Jones 6.5-6 potential, with user-defined sigma and epsilon. - LENNARD_JONES_65_6 {sigma: f64, epsilon: f64}, - /// Morse potential, with user-defined D, alpha, and r0. - MORSE{D: f64, alpha: f64, r0: f64}, - /// Tungsten-tungsten cubic spline potential (Following Bjorkas et al.) - WW, - /// Unscreened Coulombic interatomic potential between ions with charges Za and Zb. - COULOMB{Za: f64, Zb: f64} -} - -impl fmt::Display for InteractionPotential { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - InteractionPotential::TRIDYN => write!(f, "TRIDYN-style Kr-C (Different MAGIC constants)"), - InteractionPotential::MOLIERE => write!(f, "Moliere Potential"), - InteractionPotential::KR_C => write!(f, "Kr-C Potential"), - InteractionPotential::ZBL => write!(f, "ZBL Potential"), - InteractionPotential::LENZ_JENSEN => write!(f, "Lenz-Jensen Potential"), - InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => write!(f, "Lennard-Jones 12-6 Potential with sigma = {} A, epsilon = {} eV", sigma/ANGSTROM, epsilon/EV), - InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => write!(f, "Lennard-Jones 6.5-6 Potential with sigma = {} A, epsilon = {} eV", sigma/ANGSTROM, epsilon/EV), - InteractionPotential::MORSE{D, alpha, r0} => write!(f, "Morse potential with D = {} eV, alpha = {} 1/A, and r0 = {} A", D/EV, alpha*ANGSTROM, r0/ANGSTROM), - InteractionPotential::WW => write!(f, "W-W cubic spline interaction potential."), - InteractionPotential::COULOMB{Za, Zb} => write!(f, "Coulombic interaction with Za = {} and Zb = {}", Za, Zb) - } - } -} - -impl PartialEq for InteractionPotential { - fn eq(&self, other: &Self) -> bool { - discriminant(self) == discriminant(other) - } -} - -/// Method for solving the scattering integral. -#[derive(Deserialize, Clone, Copy)] -pub enum ScatteringIntegral { - /// Mendenhall-Weller Gauss-Lobatto 4-point quadrature. - MENDENHALL_WELLER, - /// Ziegler's MAGIC algorithm. - MAGIC, - /// Gauss-Mehler n-point quadrature. - GAUSS_MEHLER{n_points: usize}, - /// Gauss-Legendre 5-point quadrature. - GAUSS_LEGENDRE -} - -impl fmt::Display for ScatteringIntegral { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ScatteringIntegral::MENDENHALL_WELLER => write!(f, "Mendenhall-Weller 4-Point Lobatto Quadrature"), - ScatteringIntegral::MAGIC => write!(f, "MAGIC Algorithm"), - ScatteringIntegral::GAUSS_MEHLER{n_points} => write!(f, "Gauss-Mehler {}-point Quadrature", n_points), - ScatteringIntegral::GAUSS_LEGENDRE => write!(f, "Gauss-Legendre 5-point Quadrature"), - } - } -} - -impl PartialEq for ScatteringIntegral { - fn eq(&self, other: &Self) -> bool { - discriminant(self) == discriminant(other) - } -} - -/// Root-finding algorithm. -#[derive(Deserialize, Clone, Copy)] -pub enum Rootfinder { - /// Newton root-finder with user-defined `max_iterations` and `tolerance`. - NEWTON{max_iterations: usize, tolerance: f64}, - CPR{n0: usize, nmax: usize, epsilon: f64, complex_threshold: f64, truncation_threshold: f64, - far_from_zero: f64, interval_limit: f64, derivative_free: bool}, - POLYNOMIAL{complex_threshold: f64}, - DEFAULTNEWTON, -} - -impl fmt::Display for Rootfinder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Rootfinder::NEWTON{max_iterations, tolerance} => write!(f, "Newton-Raphson Rootfinder with maximum {} iterations and toleance = {}", max_iterations, tolerance), - Rootfinder::CPR{n0, nmax, epsilon, complex_threshold, truncation_threshold, far_from_zero, interval_limit, derivative_free} => - write!(f, "Chebyshev-Proxy Rootfinder with {}-polishing", match derivative_free { true => "Secant", false => "Newton"}), - Rootfinder::POLYNOMIAL{complex_threshold} => write!(f, "Frobenius Companion Matrix Polynomial Real Rootfinder with a complex tolerance of {}", complex_threshold), - Rootfinder::DEFAULTNEWTON => write!(f, "Newton-Raphson Rootfinder with maximum {} iterations and toleance = {}", 100, 1E-3), - } - } -} -impl PartialEq for Rootfinder { - fn eq(&self, other: &Self) -> bool { - discriminant(self) == discriminant(other) - } -} +use super::*; + +pub enum MaterialType { + MESH0D(material::Material), + MESH1D(material::Material), + MESH2D(material::Material), + SPHERE(material::Material), + #[cfg(feature = "parry3d")] + BALL(material::Material), + #[cfg(feature = "parry3d")] + TRIMESH(material::Material) +} + +#[derive(Deserialize)] +pub enum GeometryType { + MESH0D, + MESH1D, + MESH2D, + SPHERE, + #[cfg(feature = "parry3d")] + BALL, + #[cfg(feature = "parry3d")] + TRIMESH, +} + +/// Mode of electronic stopping to use. +#[derive(Deserialize, PartialEq, Clone, Copy)] +pub enum ElectronicStoppingMode { + /// Biersack-Varelas interpolated electronic stopping. Valid for ~eV/nucleon to ~GeV/nucleon. + INTERPOLATED, + /// Oen-Robinson Firsov-type local electronic stopping. Valid up to ~25 keV/nucleon. + LOW_ENERGY_LOCAL, + /// Lindhard-Scharff nonlocal electronic stopping. Valid up to ~25 keV/nucleon. + LOW_ENERGY_NONLOCAL, + /// Equipartition between Oen-Robinson and Lindhard-Scharff electronic stopping formulas. + LOW_ENERGY_EQUIPARTITION, +} + +impl fmt::Display for ElectronicStoppingMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ElectronicStoppingMode::INTERPOLATED => write!(f, "Biersack-Varelas electronic stopping"), + ElectronicStoppingMode::LOW_ENERGY_NONLOCAL => write!(f, "Lindhard-Scharff electronic stopping"), + ElectronicStoppingMode::LOW_ENERGY_LOCAL => write!(f, "Oen-Robinson electronic stopping"), + ElectronicStoppingMode::LOW_ENERGY_EQUIPARTITION => write!(f, "Equipartition with Lindhard-Scharff and Oen-Robinson"), + } + } +} + +/// Mode of surface binding energy calculation. +#[derive(Deserialize, PartialEq, Clone, Copy)] +pub enum SurfaceBindingCalculation { + /// Surface binding energies will be determined individually depending only on the particle's `Es`. + INDIVIDUAL, + /// Surface binding energies will be a concentration-weighted average of material surface-binding energies, unless `particle.Es == 0` in which case it will be zero. + TARGET, + /// Surface binding energies will be the average of the particle and TARGET, unless either is zero in which case it will be zero. + AVERAGE, +} + +impl fmt::Display for SurfaceBindingCalculation { + fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + SurfaceBindingCalculation::INDIVIDUAL => write!(f, + "Individual surface binding energies."), + SurfaceBindingCalculation::TARGET => write!(f, + "Concentration-dependent linear combinaion of target binding energies."), + SurfaceBindingCalculation::AVERAGE => write!(f, + "Average between particle and concentration-dependent linear combination of target binding energies."), + } + } +} + +#[derive(Deserialize, PartialEq, Clone, Copy)] +pub enum SurfaceBindingModel { + /// Isotropic surface binding potential - results in no refraction + ISOTROPIC{calculation: SurfaceBindingCalculation}, + /// Planar surface binding potential - particles refract through potential + PLANAR{calculation: SurfaceBindingCalculation}, + /// Planar surface binding potential default - particles refract through potential + TARGET, + INDIVIDUAL, + AVERAGE +} + +impl fmt::Display for SurfaceBindingModel { + fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + SurfaceBindingModel::ISOTROPIC{..} => write!(f, + "Locally isotropic surface binding energy."), + SurfaceBindingModel::PLANAR{..} => write!(f, + "Locally planar surface binding energy."), + SurfaceBindingModel::TARGET => write!(f, "Locally planar surface binding energy."), + SurfaceBindingModel::INDIVIDUAL =>write!(f, "Locally planar surface binding energy."), + SurfaceBindingModel::AVERAGE =>write!(f, "Locally planar surface binding energy."), + } + } +} + +/// Mode of bulk binding energy calculation. +#[derive(Deserialize, PartialEq, Clone, Copy)] +pub enum BulkBindingModel { + /// Bulk binding energies will be determined individually depending only on the particle's `Eb`. + INDIVIDUAL, + /// Bulk binding energies will be the concentration-weighted average, unless either is zero in which case it will be zero. + AVERAGE, +} + +impl fmt::Display for BulkBindingModel { + fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + BulkBindingModel::INDIVIDUAL => write!(f, + "Individual bulk binding energies."), + + BulkBindingModel::AVERAGE => write!(f, + "Concentration-weighted average bulk binding energy."), + } + } +} + +/// Mean-free-path model. +#[derive(Deserialize, PartialEq, Clone, Copy)] +pub enum MeanFreePathModel { + /// Constant mean-free-path for liquids and amorphous solids. + LIQUID, + /// Exponentially-distributed mean-free-paths for gases. + GASEOUS, + /// Switch from gas (below threshold) to liquid (above threshold). + THRESHOLD{density: f64} +} + +impl fmt::Display for MeanFreePathModel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + MeanFreePathModel::LIQUID => write!(f, "Amorphous Solid/Liquid Model"), + MeanFreePathModel::GASEOUS => write!(f, "Gaseous Model"), + MeanFreePathModel::THRESHOLD{density} => write!(f, "Gaseous if n0 < {}, Liquid/Amorphous Solid otherwise.", density) + } + } +} + +/// Interatomic potentials between particles in rustbca. +#[derive(Deserialize, Clone, Copy)] +pub enum InteractionPotential { + /// TRIDYN-style Kr-C. Equivalent to KR_C, except for the MAGIC constants. + TRIDYN, + /// Moliere's approximation to the Thomas-Fermi interatomic potential. + MOLIERE, + /// Krypton-Carbon "universal" interatomic potential. + KR_C, + /// Ziegler-Biersack-Littmark "unviversal" semi-empirical interatomic potential. + ZBL, + /// Lenz-Jensen screened Coulomb potential. + LENZ_JENSEN, + /// Lennard-Jones 12-6 potential, with user-defined sigma and epsilon. + LENNARD_JONES_12_6 {sigma: f64, epsilon: f64}, + /// Lennard-Jones 6.5-6 potential, with user-defined sigma and epsilon. + LENNARD_JONES_65_6 {sigma: f64, epsilon: f64}, + /// Morse potential, with user-defined D, alpha, and r0. + MORSE{D: f64, alpha: f64, r0: f64}, + /// Tungsten-tungsten cubic spline potential (Following Bjorkas et al.) + WW, + /// Unscreened Coulombic interatomic potential between ions with charges Za and Zb. + COULOMB{Za: f64, Zb: f64} +} + +impl fmt::Display for InteractionPotential { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + InteractionPotential::TRIDYN => write!(f, "TRIDYN-style Kr-C (Different MAGIC constants)"), + InteractionPotential::MOLIERE => write!(f, "Moliere Potential"), + InteractionPotential::KR_C => write!(f, "Kr-C Potential"), + InteractionPotential::ZBL => write!(f, "ZBL Potential"), + InteractionPotential::LENZ_JENSEN => write!(f, "Lenz-Jensen Potential"), + InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => write!(f, "Lennard-Jones 12-6 Potential with sigma = {} A, epsilon = {} eV", sigma/ANGSTROM, epsilon/EV), + InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => write!(f, "Lennard-Jones 6.5-6 Potential with sigma = {} A, epsilon = {} eV", sigma/ANGSTROM, epsilon/EV), + InteractionPotential::MORSE{D, alpha, r0} => write!(f, "Morse potential with D = {} eV, alpha = {} 1/A, and r0 = {} A", D/EV, alpha*ANGSTROM, r0/ANGSTROM), + InteractionPotential::WW => write!(f, "W-W cubic spline interaction potential."), + InteractionPotential::COULOMB{Za, Zb} => write!(f, "Coulombic interaction with Za = {} and Zb = {}", Za, Zb) + } + } +} + +impl PartialEq for InteractionPotential { + fn eq(&self, other: &Self) -> bool { + discriminant(self) == discriminant(other) + } +} + +/// Method for solving the scattering integral. +#[derive(Deserialize, Clone, Copy)] +pub enum ScatteringIntegral { + /// Mendenhall-Weller Gauss-Lobatto 4-point quadrature. + MENDENHALL_WELLER, + /// Ziegler's MAGIC algorithm. + MAGIC, + /// Gauss-Mehler n-point quadrature. + GAUSS_MEHLER{n_points: usize}, + /// Gauss-Legendre 5-point quadrature. + GAUSS_LEGENDRE +} + +impl fmt::Display for ScatteringIntegral { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ScatteringIntegral::MENDENHALL_WELLER => write!(f, "Mendenhall-Weller 4-Point Lobatto Quadrature"), + ScatteringIntegral::MAGIC => write!(f, "MAGIC Algorithm"), + ScatteringIntegral::GAUSS_MEHLER{n_points} => write!(f, "Gauss-Mehler {}-point Quadrature", n_points), + ScatteringIntegral::GAUSS_LEGENDRE => write!(f, "Gauss-Legendre 5-point Quadrature"), + } + } +} + +impl PartialEq for ScatteringIntegral { + fn eq(&self, other: &Self) -> bool { + discriminant(self) == discriminant(other) + } +} + +/// Root-finding algorithm. +#[derive(Deserialize, Clone, Copy)] +pub enum Rootfinder { + /// Newton root-finder with user-defined `max_iterations` and `tolerance`. + NEWTON{max_iterations: usize, tolerance: f64}, + CPR{n0: usize, nmax: usize, epsilon: f64, complex_threshold: f64, truncation_threshold: f64, + far_from_zero: f64, interval_limit: f64, derivative_free: bool}, + POLYNOMIAL{complex_threshold: f64}, + DEFAULTNEWTON, +} + +impl fmt::Display for Rootfinder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Rootfinder::NEWTON{max_iterations, tolerance} => write!(f, "Newton-Raphson Rootfinder with maximum {} iterations and toleance = {}", max_iterations, tolerance), + Rootfinder::CPR{n0, nmax, epsilon, complex_threshold, truncation_threshold, far_from_zero, interval_limit, derivative_free} => + write!(f, "Chebyshev-Proxy Rootfinder with {}-polishing", match derivative_free { true => "Secant", false => "Newton"}), + Rootfinder::POLYNOMIAL{complex_threshold} => write!(f, "Frobenius Companion Matrix Polynomial Real Rootfinder with a complex tolerance of {}", complex_threshold), + Rootfinder::DEFAULTNEWTON => write!(f, "Newton-Raphson Rootfinder with maximum {} iterations and toleance = {}", 100, 1E-3), + } + } +} +impl PartialEq for Rootfinder { + fn eq(&self, other: &Self) -> bool { + discriminant(self) == discriminant(other) + } +} diff --git a/src/geometry.rs b/src/geometry.rs index 1e10a3d..462bad5 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,652 +1,652 @@ -use super::*; - -use geo::algorithm::contains::Contains; -use geo::{Polygon, LineString, Point, point, Closest}; -use geo::algorithm::closest_point::ClosestPoint; - -///Trait for a Geometry object - all forms of geometry must implement these traits to be used -pub trait Geometry { - - type InputFileFormat: InputFile + Clone; - - fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Self; - fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec; - fn get_ck(&self, x: f64, y: f64, z: f64) -> f64; - fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64; - fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec; - fn inside(&self, x: f64, y: f64, z: f64) -> bool; - fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool; - fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool; - fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64); -} - -pub trait GeometryElement: Clone { - fn contains(&self, x: f64, y: f64, z: f64) -> bool; - fn distance_to(&self, x: f64, y: f64, z: f64) -> f64; - fn get_densities(&self) -> &Vec; - fn get_concentrations(&self) -> &Vec; - fn get_electronic_stopping_correction_factor(&self) -> f64; -} - -#[derive(Deserialize, Clone)] -pub struct Mesh0DInput { - pub length_unit: String, - pub densities: Vec, - pub electronic_stopping_correction_factor: f64, -} - -#[derive(Clone)] -pub struct Mesh0D { - pub densities: Vec, - pub concentrations: Vec, - pub electronic_stopping_correction_factor: f64, - pub energy_barrier_thickness: f64, -} - -impl Geometry for Mesh0D { - - type InputFileFormat = Input0D; - - fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Mesh0D { - - let length_unit: f64 = match input.length_unit.as_str() { - "MICRON" => MICRON, - "CM" => CM, - "ANGSTROM" => ANGSTROM, - "NM" => NM, - "M" => 1., - _ => input.length_unit.parse() - .expect(format!( - "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", - &input.length_unit.as_str() - ).as_str()), - }; - - let electronic_stopping_correction_factor = input.electronic_stopping_correction_factor; - - let densities: Vec = input.densities.iter().map(|element| element/(length_unit).powi(3)).collect(); - - let total_density: f64 = densities.iter().sum(); - - let energy_barrier_thickness = total_density.powf(-1./3.)/SQRTPI*2.; - - let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); - - Mesh0D { - densities, - concentrations, - electronic_stopping_correction_factor, - energy_barrier_thickness - } - } - - fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { - &self.densities - } - fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { - self.electronic_stopping_correction_factor - } - fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64{ - self.densities.iter().sum() - } - fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { - &self.concentrations - } - fn inside(&self, x: f64, y: f64, z: f64) -> bool { - x > 0.0 - } - - fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { - //println!("x: {} energy_barrier_thickness: {}", x/ANGSTROM, self.energy_barrier_thickness/ANGSTROM); - x > -10.*self.energy_barrier_thickness - } - - fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { - x > -self.energy_barrier_thickness - } - - fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { - (0., y, z) - } -} - -#[derive(Deserialize, Clone)] -pub struct Mesh1DInput { - pub length_unit: String, - pub layer_thicknesses: Vec, - pub densities: Vec>, - pub electronic_stopping_correction_factors: Vec, -} - -#[derive(Clone)] -pub struct Mesh1D { - layers: Vec, - top: f64, - bottom: f64, - pub top_energy_barrier_thickness: f64, - pub bottom_energy_barrier_thickness: f64, -} - -impl Geometry for Mesh1D { - - type InputFileFormat = input::Input1D; - - fn new(geometry_input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Self { - - let layer_thicknesses = geometry_input.layer_thicknesses.clone(); - let electronic_stopping_correction_factors = geometry_input.electronic_stopping_correction_factors.clone(); - let n = layer_thicknesses.len(); - - let mut layers: Vec = Vec::with_capacity(n); - - //Multiply all coordinates by value of geometry unit. - let length_unit: f64 = match geometry_input.length_unit.as_str() { - "MICRON" => MICRON, - "CM" => CM, - "ANGSTROM" => ANGSTROM, - "NM" => NM, - "M" => 1., - _ => geometry_input.length_unit.parse() - .expect(format!( - "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", &geometry_input.length_unit.as_str() - ).as_str()), - }; - - let densities: Vec> = geometry_input.densities - .iter() - .map( |row| row.iter().map(|element| element/(length_unit).powi(3)).collect() ).collect(); - - //Assert all layer density lists are equal length - assert!( - densities.iter() - .all(|density_list| density_list.len() == densities[0].len()), - "Input error: all layer density lists must be the same size." - ); - assert_eq!(layer_thicknesses.len(), densities.len(), "Input error: coordinates and data of unequal length."); - - let mut layer_top = 0.; - let mut layer_bottom = 0.; - for ((layer_thickness, densities), ck) in layer_thicknesses.iter().zip(densities).zip(electronic_stopping_correction_factors) { - - layer_bottom += layer_thickness*length_unit; - - let total_density: f64 = densities.iter().sum(); - let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); - - layers.push(Layer1D::new(layer_top, layer_bottom, densities, concentrations, ck)); - layer_top = layer_bottom; - } - - let top_energy_barrier_thickness = layers[0].densities.iter().sum::().powf(-1./3.)/SQRTPI*2.; - let bottom_energy_barrier_thickness = layers[layers.len() - 1].densities.iter().sum::().powf(-1./3.)/SQRTPI*2.; - - Mesh1D { - layers, - top: 0.0, - bottom: layer_bottom, - top_energy_barrier_thickness, - bottom_energy_barrier_thickness, - } - } - fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { - if self.inside(x, y, z) { - for layer in &self.layers { - if layer.contains(x, y, z) { - return &layer.densities - } - } - panic!("Geometry error: method inside() is returning true for points outside all layers. Check geometry.") - } else if x < self.top { - &self.layers[0].densities - } else { - &self.layers[self.layers.len() - 1].densities - } - } - fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { - if self.inside(x, y, z) { - for layer in &self.layers { - if layer.contains(x, y, z) { - return layer.electronic_stopping_correction_factor - } - } - panic!("Geometry error: method inside() is returning true for points outside all layers. Check geometry.") - } else if x < self.top { - self.layers[0].electronic_stopping_correction_factor - } else { - self.layers[self.layers.len() - 1].electronic_stopping_correction_factor - } - } - fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64 { - if self.inside(x, y, z) { - for layer in &self.layers { - if layer.contains(x, y, z) { - return layer.densities.iter().sum() - } - } - panic!("Geometry error: method inside() is returning true for points outside all layers. Check geometry.") - } else if x < self.top { - self.layers[0].densities.iter().sum() - } else { - self.layers[self.layers.len() - 1].densities.iter().sum() - } - } - fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { - if self.inside(x, y, z) { - for layer in &self.layers { - if layer.contains(x, y, z) { - return &layer.concentrations - } - } - panic!("Geometry error: method inside() is returning true for points outside all layers. Check geometry.") - } else if x < self.top { - &self.layers[0].concentrations - } else { - &self.layers[self.layers.len() - 1].concentrations - } - } - fn inside(&self, x: f64, y: f64, z: f64) -> bool { - (x > self.top) & (x < self.bottom) - } - fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { - (x > self.top - 10.*self.top_energy_barrier_thickness) & (x < self.bottom + 10.*self.bottom_energy_barrier_thickness) - } - fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { - (x > self.top - self.top_energy_barrier_thickness) & (x < self.bottom + self.bottom_energy_barrier_thickness) - } - fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { - let distance_from_bottom = (x - self.bottom).abs(); - let distance_from_top = (x - self.top).abs(); - if distance_from_bottom < distance_from_top { - (self.bottom, y, z) - } else { - (self.top, y, z) - } - } -} - -/// Object that contains raw mesh input data. -#[derive(Deserialize, Clone)] -pub struct Mesh2DInput { - pub length_unit: String, - pub triangles: Vec<(f64, f64, f64, f64, f64, f64)>, - pub material_boundary_points: Vec<(f64, f64)>, - pub simulation_boundary_points: Vec<(f64, f64)>, - pub densities: Vec>, - pub electronic_stopping_correction_factors: Vec, - pub energy_barrier_thickness: f64, -} - -/// Triangular mesh for rustbca. -#[derive(Clone)] -pub struct Mesh2D { - mesh: Vec, - pub boundary: Polygon, - pub simulation_boundary: Polygon, - pub energy_barrier_thickness: f64 -} - -impl Mesh2D { - /// Finds the cell that is nearest to (x, y). - fn nearest_to(&self, x: f64, y: f64, z: f64) -> &Cell2D { - - let mut min_distance: f64 = std::f64::MAX; - let mut index: usize = 0; - - for (cell_index, cell) in self.mesh.iter().enumerate() { - let distance_to = cell.distance_to(x, y, z); - if distance_to < min_distance { - min_distance = distance_to; - index = cell_index; - } - } - - return &self.mesh[index]; - } -} - -impl Geometry for Mesh2D { - - type InputFileFormat = Input2D; - - /// Constructor for Mesh2D object from geometry_input. - fn new(geometry_input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Mesh2D { - - let triangles = geometry_input.triangles.clone(); - let material_boundary_points = geometry_input.material_boundary_points.clone(); - let simulation_boundary_points = geometry_input.simulation_boundary_points.clone(); - let electronic_stopping_correction_factors = geometry_input.electronic_stopping_correction_factors.clone(); - - let n = triangles.len(); - - let mut cells: Vec = Vec::with_capacity(n); - - //Multiply all coordinates by value of geometry unit. - let length_unit: f64 = match geometry_input.length_unit.as_str() { - "MICRON" => MICRON, - "CM" => CM, - "ANGSTROM" => ANGSTROM, - "NM" => NM, - "M" => 1., - _ => geometry_input.length_unit.parse() - .expect(format!( - "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", &geometry_input.length_unit.as_str() - ).as_str()), - }; - - let densities: Vec> = geometry_input.densities - .iter() - .map( |row| row.iter().map(|element| element/(length_unit).powi(3)).collect() ).collect(); - - //Assert all triangle density lists are equal length - assert!( - densities.iter() - .all(|density_list| density_list.len() == densities[0].len()), - "Input error: all triangle density lists must be the same size." - ); - - assert_eq!(triangles.len(), densities.len(), "Input error: coordinates and data of unequal length."); - - for ((coordinate_set, densities), ck) in triangles.iter().zip(densities).zip(electronic_stopping_correction_factors) { - let coordinate_set_converted = ( - coordinate_set.0*length_unit, - coordinate_set.1*length_unit, - coordinate_set.2*length_unit, - coordinate_set.3*length_unit, - coordinate_set.4*length_unit, - coordinate_set.5*length_unit, - ); - - let total_density: f64 = densities.iter().sum(); - let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); - - cells.push(Cell2D::new(coordinate_set_converted, densities, concentrations, ck)); - } - - let mut boundary_points_converted = Vec::with_capacity(material_boundary_points.len()); - for (x, y) in material_boundary_points.iter() { - boundary_points_converted.push((x*length_unit, y*length_unit)); - } - let boundary: Polygon = Polygon::new(LineString::from(boundary_points_converted), vec![]); - - let mut simulation_boundary_points_converted = Vec::with_capacity(simulation_boundary_points.len()); - for (x, y) in simulation_boundary_points.iter() { - simulation_boundary_points_converted.push((x*length_unit, y*length_unit)); - } - let simulation_boundary: Polygon = Polygon::new(LineString::from(simulation_boundary_points_converted), vec![]); - - let energy_barrier_thickness = geometry_input.energy_barrier_thickness*length_unit; - - Mesh2D { - mesh: cells, - boundary: boundary, - simulation_boundary: simulation_boundary, - energy_barrier_thickness: energy_barrier_thickness, - } - } - - fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { - if self.inside(x, y, z) { - true - } else { - let nearest_cell = self.nearest_to(x, y, z); - let distance = nearest_cell.distance_to(x, y, z); - distance < self.energy_barrier_thickness - } - } - - fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { - self.simulation_boundary.contains(&point!(x: x, y: y)) - } - - fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { - if let Closest::SinglePoint(p) = self.boundary.closest_point(&point!(x: x, y: y)) { - (p.x(), p.y(), z) - } else { - panic!("Geometry error: closest point routine failed to find single closest point to ({}, {}, {}).", x, y, z); - } - } - - /// Find the number densities of the triangle that contains or is nearest to (x, y). - fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { - if self.inside(x, y, z) { - for cell in &self.mesh { - if cell.contains(x, y, z) { - return &cell.densities; - } - } - panic!("Geometry error: point ({}, {}) not found in any cell of the mesh.", x, y); - } else { - self.nearest_to(x, y, z).get_densities() - } - } - - /// Find the number densities of the triangle that contains or is nearest to (x, y). - fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { - if self.inside(x, y, z) { - for cell in &self.mesh { - if cell.contains(x, y, z) { - return cell.electronic_stopping_correction_factor; - } - } - panic!("Geometry error: point ({}, {}) not found in any cell of the mesh.", x, y); - } else { - return self.nearest_to(x, y, z).electronic_stopping_correction_factor; - } - } - - /// Determine the total number density of the triangle that contains or is nearest to (x, y). - fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64 { - if self.inside(x, y, z) { - for cell in &self.mesh { - if cell.contains(x, y, z) { - return cell.densities.iter().sum::(); - } - } - panic!("Geometry error: point ({}, {}) not found in any cell of the mesh.", x, y); - } else { - return self.nearest_to(x, y, z).densities.iter().sum::(); - } - } - - /// Find the concentrations of the triangle that contains or is nearest to (x, y). - fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { - if self.inside(x, y, z) { - for cell in &self.mesh { - if cell.contains(x, y, z) { - return &cell.concentrations; - } - } - panic!("Geometry error: method inside() is returning true for points outside all cells. Check boundary points.") - } else { - return &self.nearest_to(x, y, z).concentrations; - } - } - - /// Determines whether the point (x, y) is inside the mesh. - fn inside(&self, x: f64, y: f64, z: f64) -> bool { - self.boundary.contains(&Point::new(x, y)) - } -} - -/// A mesh cell that contains a triangle and the local number densities and concentrations. -#[derive(Clone)] -pub struct Cell2D { - triangle: Triangle2D, - pub densities: Vec, - pub concentrations: Vec, - pub electronic_stopping_correction_factor: f64 -} -impl Cell2D { - /// Constructor for Cell2D from a set of coordinates and a list of densities and concentrations. - pub fn new(coordinate_set: (f64, f64, f64, f64, f64, f64), densities: Vec, concentrations: Vec, electronic_stopping_correction_factor: f64) -> Cell2D { - Cell2D { - triangle: Triangle2D::new(coordinate_set), - densities, - concentrations, - electronic_stopping_correction_factor - } - } -} - -impl GeometryElement for Cell2D { - /// Determines whether this cell contains the point (x, y). - fn contains(&self, x: f64, y: f64, z: f64) -> bool { - self.triangle.contains(x, y) - } - - /// Computes the shortest distance between this cell and the point (x, y). - fn distance_to(&self, x: f64, y: f64, z: f64) -> f64 { - self.triangle.distance_to(x, y) - } - - fn get_densities(&self) -> &Vec { - &self.densities - } - - fn get_concentrations(&self) -> &Vec { - &self.concentrations - } - - fn get_electronic_stopping_correction_factor(&self) -> f64 { - self.electronic_stopping_correction_factor - } -} - -#[derive(Clone)] -pub struct Layer1D { - top: f64, - bottom: f64, - densities: Vec, - concentrations: Vec, - electronic_stopping_correction_factor: f64 -} - -impl Layer1D { - pub fn new(top: f64, bottom: f64, densities: Vec, concentrations: Vec, electronic_stopping_correction_factor: f64) -> Layer1D { - - assert!(top < bottom, "Layer cannot have negative thickness."); - - Layer1D{ - top, - bottom, - densities, - concentrations, - electronic_stopping_correction_factor - } - } -} - -impl GeometryElement for Layer1D { - fn contains(&self, x: f64, y: f64, z: f64) -> bool { - (x > self.top) & (x <= self.bottom) - } - fn distance_to(&self, x: f64, y: f64, z: f64) -> f64 { - let distance_to_top = (x - self.top).abs(); - let distance_to_bottom = (x - self.bottom).abs(); - if distance_to_top < distance_to_bottom { - distance_to_top - } else { - distance_to_bottom - } - } - fn get_densities(&self) -> &Vec { - &self.densities - } - fn get_concentrations(&self) -> &Vec { - &self.concentrations - } - fn get_electronic_stopping_correction_factor(&self) -> f64 { - self.electronic_stopping_correction_factor - } -} - -/// A triangle in 2D, with points (x1, y1), (x2, y2), (x3, y3), and the three line segments bewtween them. -#[derive(Clone)] -pub struct Triangle2D { - x1: f64, - x2: f64, - x3: f64, - y1: f64, - y2: f64, - y3: f64, - segments: Vec<(f64, f64, f64, f64)> -} -impl Triangle2D { - /// Constructor for a triangle from a list of coordinates (x1, x2, x3, y1, y2, y3). - pub fn new(coordinate_set: (f64, f64, f64, f64, f64, f64)) -> Triangle2D { - - let x1 = coordinate_set.0; - let x2 = coordinate_set.1; - let x3 = coordinate_set.2; - - let y1 = coordinate_set.3; - let y2 = coordinate_set.4; - let y3 = coordinate_set.5; - - Triangle2D { - x1, - x2, - x3, - y1, - y2, - y3, - segments: vec![(x1, y1, x2, y2), (x2, y2, x3, y3), (x3, y3, x1, y1)] - } - } - - /// Determines whether the point (x, y) is inside this triangle. - pub fn contains(&self, x: f64, y: f64) -> bool { - let x1 = self.x1; - let x2 = self.x2; - let x3 = self.x3; - let y1 = self.y1; - let y2 = self.y2; - let y3 = self.y3; - - let a = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3)) / ((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3)); - let b = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3)) / ((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3)); - let c = 1. - a - b; - - (0. <= a) & (a <= 1.) & (0. <= b) & (b <= 1.) & (0. <= c) & (c <= 1.) - } - - /// Returns a point (x, y) that is the centroid of the triangle. - pub fn centroid(&self) -> (f64, f64) { - ((self.x1 + self.x2 + self.x3)/3., (self.y1 + self.y2 + self.y3)/3.) - } - - /// Calculates the shortest distance from this triangle to the point (x, y). - pub fn distance_to(&self, x: f64, y: f64) -> f64 { - let mut distance_to = std::f64::MAX; - - for segment in &self.segments { - let length_2 = (segment.2 - segment.0)*(segment.2 - segment.0) + (segment.3 - segment.1)*(segment.3 - segment.1); - - assert!(length_2 != 0., "Geometry error: mesh contains triangle with zero-length side."); - - let u = ((x - segment.0)*(segment.2 - segment.0) + (y - segment.1)*(segment.3 - segment.1))/length_2; - - let distance = if u < 0. { - ((x - segment.0)*(x - segment.0) + (y - segment.1)*(y - segment.1)).sqrt() - } else if u > 1. { - ((x - segment.2)*(x - segment.2) + (y - segment.3)*(y - segment.3)).sqrt() - } else { - let x_intersect = segment.0 + u*(segment.2 - segment.0); - let y_intersect = segment.1 + u*(segment.3 - segment.1); - - ((x - x_intersect)*(x - x_intersect) + (y - y_intersect)*(y - y_intersect)).sqrt() - }; - - if distance < distance_to { - distance_to = distance; - } - } - distance_to - } - - /// Calculates the distance between the center of this triangle and the point (x, y). - pub fn distance_to_center(&self, x: f64, y: f64) -> f64 { - let centroid = self.centroid(); - ((x - centroid.0)*(x - centroid.0) + (y - centroid.1)*(y - centroid.1)).sqrt() - } -} +use super::*; + +use geo::algorithm::contains::Contains; +use geo::{Polygon, LineString, Point, point, Closest}; +use geo::algorithm::closest_point::ClosestPoint; + +///Trait for a Geometry object - all forms of geometry must implement these traits to be used +pub trait Geometry { + + type InputFileFormat: InputFile + Clone; + + fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Self; + fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec; + fn get_ck(&self, x: f64, y: f64, z: f64) -> f64; + fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64; + fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec; + fn inside(&self, x: f64, y: f64, z: f64) -> bool; + fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool; + fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool; + fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64); +} + +pub trait GeometryElement: Clone { + fn contains(&self, x: f64, y: f64, z: f64) -> bool; + fn distance_to(&self, x: f64, y: f64, z: f64) -> f64; + fn get_densities(&self) -> &Vec; + fn get_concentrations(&self) -> &Vec; + fn get_electronic_stopping_correction_factor(&self) -> f64; +} + +#[derive(Deserialize, Clone)] +pub struct Mesh0DInput { + pub length_unit: String, + pub densities: Vec, + pub electronic_stopping_correction_factor: f64, +} + +#[derive(Clone)] +pub struct Mesh0D { + pub densities: Vec, + pub concentrations: Vec, + pub electronic_stopping_correction_factor: f64, + pub energy_barrier_thickness: f64, +} + +impl Geometry for Mesh0D { + + type InputFileFormat = Input0D; + + fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Mesh0D { + + let length_unit: f64 = match input.length_unit.as_str() { + "MICRON" => MICRON, + "CM" => CM, + "ANGSTROM" => ANGSTROM, + "NM" => NM, + "M" => 1., + _ => input.length_unit.parse() + .expect(format!( + "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", + &input.length_unit.as_str() + ).as_str()), + }; + + let electronic_stopping_correction_factor = input.electronic_stopping_correction_factor; + + let densities: Vec = input.densities.iter().map(|element| element/(length_unit).powi(3)).collect(); + + let total_density: f64 = densities.iter().sum(); + + let energy_barrier_thickness = total_density.powf(-1./3.)/SQRTPI*2.; + + let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); + + Mesh0D { + densities, + concentrations, + electronic_stopping_correction_factor, + energy_barrier_thickness + } + } + + fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { + &self.densities + } + fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { + self.electronic_stopping_correction_factor + } + fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64{ + self.densities.iter().sum() + } + fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { + &self.concentrations + } + fn inside(&self, x: f64, y: f64, z: f64) -> bool { + x > 0.0 + } + + fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { + //println!("x: {} energy_barrier_thickness: {}", x/ANGSTROM, self.energy_barrier_thickness/ANGSTROM); + x > -10.*self.energy_barrier_thickness + } + + fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { + x > -self.energy_barrier_thickness + } + + fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { + (0., y, z) + } +} + +#[derive(Deserialize, Clone)] +pub struct Mesh1DInput { + pub length_unit: String, + pub layer_thicknesses: Vec, + pub densities: Vec>, + pub electronic_stopping_correction_factors: Vec, +} + +#[derive(Clone)] +pub struct Mesh1D { + layers: Vec, + top: f64, + bottom: f64, + pub top_energy_barrier_thickness: f64, + pub bottom_energy_barrier_thickness: f64, +} + +impl Geometry for Mesh1D { + + type InputFileFormat = input::Input1D; + + fn new(geometry_input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Self { + + let layer_thicknesses = geometry_input.layer_thicknesses.clone(); + let electronic_stopping_correction_factors = geometry_input.electronic_stopping_correction_factors.clone(); + let n = layer_thicknesses.len(); + + let mut layers: Vec = Vec::with_capacity(n); + + //Multiply all coordinates by value of geometry unit. + let length_unit: f64 = match geometry_input.length_unit.as_str() { + "MICRON" => MICRON, + "CM" => CM, + "ANGSTROM" => ANGSTROM, + "NM" => NM, + "M" => 1., + _ => geometry_input.length_unit.parse() + .expect(format!( + "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", &geometry_input.length_unit.as_str() + ).as_str()), + }; + + let densities: Vec> = geometry_input.densities + .iter() + .map( |row| row.iter().map(|element| element/(length_unit).powi(3)).collect() ).collect(); + + //Assert all layer density lists are equal length + assert!( + densities.iter() + .all(|density_list| density_list.len() == densities[0].len()), + "Input error: all layer density lists must be the same size." + ); + assert_eq!(layer_thicknesses.len(), densities.len(), "Input error: coordinates and data of unequal length."); + + let mut layer_top = 0.; + let mut layer_bottom = 0.; + for ((layer_thickness, densities), ck) in layer_thicknesses.iter().zip(densities).zip(electronic_stopping_correction_factors) { + + layer_bottom += layer_thickness*length_unit; + + let total_density: f64 = densities.iter().sum(); + let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); + + layers.push(Layer1D::new(layer_top, layer_bottom, densities, concentrations, ck)); + layer_top = layer_bottom; + } + + let top_energy_barrier_thickness = layers[0].densities.iter().sum::().powf(-1./3.)/SQRTPI*2.; + let bottom_energy_barrier_thickness = layers[layers.len() - 1].densities.iter().sum::().powf(-1./3.)/SQRTPI*2.; + + Mesh1D { + layers, + top: 0.0, + bottom: layer_bottom, + top_energy_barrier_thickness, + bottom_energy_barrier_thickness, + } + } + fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { + if self.inside(x, y, z) { + for layer in &self.layers { + if layer.contains(x, y, z) { + return &layer.densities + } + } + panic!("Geometry error: method inside() is returning true for points outside all layers. Check geometry.") + } else if x < self.top { + &self.layers[0].densities + } else { + &self.layers[self.layers.len() - 1].densities + } + } + fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { + if self.inside(x, y, z) { + for layer in &self.layers { + if layer.contains(x, y, z) { + return layer.electronic_stopping_correction_factor + } + } + panic!("Geometry error: method inside() is returning true for points outside all layers. Check geometry.") + } else if x < self.top { + self.layers[0].electronic_stopping_correction_factor + } else { + self.layers[self.layers.len() - 1].electronic_stopping_correction_factor + } + } + fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64 { + if self.inside(x, y, z) { + for layer in &self.layers { + if layer.contains(x, y, z) { + return layer.densities.iter().sum() + } + } + panic!("Geometry error: method inside() is returning true for points outside all layers. Check geometry.") + } else if x < self.top { + self.layers[0].densities.iter().sum() + } else { + self.layers[self.layers.len() - 1].densities.iter().sum() + } + } + fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { + if self.inside(x, y, z) { + for layer in &self.layers { + if layer.contains(x, y, z) { + return &layer.concentrations + } + } + panic!("Geometry error: method inside() is returning true for points outside all layers. Check geometry.") + } else if x < self.top { + &self.layers[0].concentrations + } else { + &self.layers[self.layers.len() - 1].concentrations + } + } + fn inside(&self, x: f64, y: f64, z: f64) -> bool { + (x > self.top) & (x < self.bottom) + } + fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { + (x > self.top - 10.*self.top_energy_barrier_thickness) & (x < self.bottom + 10.*self.bottom_energy_barrier_thickness) + } + fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { + (x > self.top - self.top_energy_barrier_thickness) & (x < self.bottom + self.bottom_energy_barrier_thickness) + } + fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { + let distance_from_bottom = (x - self.bottom).abs(); + let distance_from_top = (x - self.top).abs(); + if distance_from_bottom < distance_from_top { + (self.bottom, y, z) + } else { + (self.top, y, z) + } + } +} + +/// Object that contains raw mesh input data. +#[derive(Deserialize, Clone)] +pub struct Mesh2DInput { + pub length_unit: String, + pub triangles: Vec<(f64, f64, f64, f64, f64, f64)>, + pub material_boundary_points: Vec<(f64, f64)>, + pub simulation_boundary_points: Vec<(f64, f64)>, + pub densities: Vec>, + pub electronic_stopping_correction_factors: Vec, + pub energy_barrier_thickness: f64, +} + +/// Triangular mesh for rustbca. +#[derive(Clone)] +pub struct Mesh2D { + mesh: Vec, + pub boundary: Polygon, + pub simulation_boundary: Polygon, + pub energy_barrier_thickness: f64 +} + +impl Mesh2D { + /// Finds the cell that is nearest to (x, y). + fn nearest_to(&self, x: f64, y: f64, z: f64) -> &Cell2D { + + let mut min_distance: f64 = std::f64::MAX; + let mut index: usize = 0; + + for (cell_index, cell) in self.mesh.iter().enumerate() { + let distance_to = cell.distance_to(x, y, z); + if distance_to < min_distance { + min_distance = distance_to; + index = cell_index; + } + } + + return &self.mesh[index]; + } +} + +impl Geometry for Mesh2D { + + type InputFileFormat = Input2D; + + /// Constructor for Mesh2D object from geometry_input. + fn new(geometry_input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Mesh2D { + + let triangles = geometry_input.triangles.clone(); + let material_boundary_points = geometry_input.material_boundary_points.clone(); + let simulation_boundary_points = geometry_input.simulation_boundary_points.clone(); + let electronic_stopping_correction_factors = geometry_input.electronic_stopping_correction_factors.clone(); + + let n = triangles.len(); + + let mut cells: Vec = Vec::with_capacity(n); + + //Multiply all coordinates by value of geometry unit. + let length_unit: f64 = match geometry_input.length_unit.as_str() { + "MICRON" => MICRON, + "CM" => CM, + "ANGSTROM" => ANGSTROM, + "NM" => NM, + "M" => 1., + _ => geometry_input.length_unit.parse() + .expect(format!( + "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", &geometry_input.length_unit.as_str() + ).as_str()), + }; + + let densities: Vec> = geometry_input.densities + .iter() + .map( |row| row.iter().map(|element| element/(length_unit).powi(3)).collect() ).collect(); + + //Assert all triangle density lists are equal length + assert!( + densities.iter() + .all(|density_list| density_list.len() == densities[0].len()), + "Input error: all triangle density lists must be the same size." + ); + + assert_eq!(triangles.len(), densities.len(), "Input error: coordinates and data of unequal length."); + + for ((coordinate_set, densities), ck) in triangles.iter().zip(densities).zip(electronic_stopping_correction_factors) { + let coordinate_set_converted = ( + coordinate_set.0*length_unit, + coordinate_set.1*length_unit, + coordinate_set.2*length_unit, + coordinate_set.3*length_unit, + coordinate_set.4*length_unit, + coordinate_set.5*length_unit, + ); + + let total_density: f64 = densities.iter().sum(); + let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); + + cells.push(Cell2D::new(coordinate_set_converted, densities, concentrations, ck)); + } + + let mut boundary_points_converted = Vec::with_capacity(material_boundary_points.len()); + for (x, y) in material_boundary_points.iter() { + boundary_points_converted.push((x*length_unit, y*length_unit)); + } + let boundary: Polygon = Polygon::new(LineString::from(boundary_points_converted), vec![]); + + let mut simulation_boundary_points_converted = Vec::with_capacity(simulation_boundary_points.len()); + for (x, y) in simulation_boundary_points.iter() { + simulation_boundary_points_converted.push((x*length_unit, y*length_unit)); + } + let simulation_boundary: Polygon = Polygon::new(LineString::from(simulation_boundary_points_converted), vec![]); + + let energy_barrier_thickness = geometry_input.energy_barrier_thickness*length_unit; + + Mesh2D { + mesh: cells, + boundary: boundary, + simulation_boundary: simulation_boundary, + energy_barrier_thickness: energy_barrier_thickness, + } + } + + fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { + if self.inside(x, y, z) { + true + } else { + let nearest_cell = self.nearest_to(x, y, z); + let distance = nearest_cell.distance_to(x, y, z); + distance < self.energy_barrier_thickness + } + } + + fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { + self.simulation_boundary.contains(&point!(x: x, y: y)) + } + + fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { + if let Closest::SinglePoint(p) = self.boundary.closest_point(&point!(x: x, y: y)) { + (p.x(), p.y(), z) + } else { + panic!("Geometry error: closest point routine failed to find single closest point to ({}, {}, {}).", x, y, z); + } + } + + /// Find the number densities of the triangle that contains or is nearest to (x, y). + fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { + if self.inside(x, y, z) { + for cell in &self.mesh { + if cell.contains(x, y, z) { + return &cell.densities; + } + } + panic!("Geometry error: point ({}, {}) not found in any cell of the mesh.", x, y); + } else { + self.nearest_to(x, y, z).get_densities() + } + } + + /// Find the number densities of the triangle that contains or is nearest to (x, y). + fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { + if self.inside(x, y, z) { + for cell in &self.mesh { + if cell.contains(x, y, z) { + return cell.electronic_stopping_correction_factor; + } + } + panic!("Geometry error: point ({}, {}) not found in any cell of the mesh.", x, y); + } else { + return self.nearest_to(x, y, z).electronic_stopping_correction_factor; + } + } + + /// Determine the total number density of the triangle that contains or is nearest to (x, y). + fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64 { + if self.inside(x, y, z) { + for cell in &self.mesh { + if cell.contains(x, y, z) { + return cell.densities.iter().sum::(); + } + } + panic!("Geometry error: point ({}, {}) not found in any cell of the mesh.", x, y); + } else { + return self.nearest_to(x, y, z).densities.iter().sum::(); + } + } + + /// Find the concentrations of the triangle that contains or is nearest to (x, y). + fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { + if self.inside(x, y, z) { + for cell in &self.mesh { + if cell.contains(x, y, z) { + return &cell.concentrations; + } + } + panic!("Geometry error: method inside() is returning true for points outside all cells. Check boundary points.") + } else { + return &self.nearest_to(x, y, z).concentrations; + } + } + + /// Determines whether the point (x, y) is inside the mesh. + fn inside(&self, x: f64, y: f64, z: f64) -> bool { + self.boundary.contains(&Point::new(x, y)) + } +} + +/// A mesh cell that contains a triangle and the local number densities and concentrations. +#[derive(Clone)] +pub struct Cell2D { + triangle: Triangle2D, + pub densities: Vec, + pub concentrations: Vec, + pub electronic_stopping_correction_factor: f64 +} +impl Cell2D { + /// Constructor for Cell2D from a set of coordinates and a list of densities and concentrations. + pub fn new(coordinate_set: (f64, f64, f64, f64, f64, f64), densities: Vec, concentrations: Vec, electronic_stopping_correction_factor: f64) -> Cell2D { + Cell2D { + triangle: Triangle2D::new(coordinate_set), + densities, + concentrations, + electronic_stopping_correction_factor + } + } +} + +impl GeometryElement for Cell2D { + /// Determines whether this cell contains the point (x, y). + fn contains(&self, x: f64, y: f64, z: f64) -> bool { + self.triangle.contains(x, y) + } + + /// Computes the shortest distance between this cell and the point (x, y). + fn distance_to(&self, x: f64, y: f64, z: f64) -> f64 { + self.triangle.distance_to(x, y) + } + + fn get_densities(&self) -> &Vec { + &self.densities + } + + fn get_concentrations(&self) -> &Vec { + &self.concentrations + } + + fn get_electronic_stopping_correction_factor(&self) -> f64 { + self.electronic_stopping_correction_factor + } +} + +#[derive(Clone)] +pub struct Layer1D { + top: f64, + bottom: f64, + densities: Vec, + concentrations: Vec, + electronic_stopping_correction_factor: f64 +} + +impl Layer1D { + pub fn new(top: f64, bottom: f64, densities: Vec, concentrations: Vec, electronic_stopping_correction_factor: f64) -> Layer1D { + + assert!(top < bottom, "Layer cannot have negative thickness."); + + Layer1D{ + top, + bottom, + densities, + concentrations, + electronic_stopping_correction_factor + } + } +} + +impl GeometryElement for Layer1D { + fn contains(&self, x: f64, y: f64, z: f64) -> bool { + (x > self.top) & (x <= self.bottom) + } + fn distance_to(&self, x: f64, y: f64, z: f64) -> f64 { + let distance_to_top = (x - self.top).abs(); + let distance_to_bottom = (x - self.bottom).abs(); + if distance_to_top < distance_to_bottom { + distance_to_top + } else { + distance_to_bottom + } + } + fn get_densities(&self) -> &Vec { + &self.densities + } + fn get_concentrations(&self) -> &Vec { + &self.concentrations + } + fn get_electronic_stopping_correction_factor(&self) -> f64 { + self.electronic_stopping_correction_factor + } +} + +/// A triangle in 2D, with points (x1, y1), (x2, y2), (x3, y3), and the three line segments bewtween them. +#[derive(Clone)] +pub struct Triangle2D { + x1: f64, + x2: f64, + x3: f64, + y1: f64, + y2: f64, + y3: f64, + segments: Vec<(f64, f64, f64, f64)> +} +impl Triangle2D { + /// Constructor for a triangle from a list of coordinates (x1, x2, x3, y1, y2, y3). + pub fn new(coordinate_set: (f64, f64, f64, f64, f64, f64)) -> Triangle2D { + + let x1 = coordinate_set.0; + let x2 = coordinate_set.1; + let x3 = coordinate_set.2; + + let y1 = coordinate_set.3; + let y2 = coordinate_set.4; + let y3 = coordinate_set.5; + + Triangle2D { + x1, + x2, + x3, + y1, + y2, + y3, + segments: vec![(x1, y1, x2, y2), (x2, y2, x3, y3), (x3, y3, x1, y1)] + } + } + + /// Determines whether the point (x, y) is inside this triangle. + pub fn contains(&self, x: f64, y: f64) -> bool { + let x1 = self.x1; + let x2 = self.x2; + let x3 = self.x3; + let y1 = self.y1; + let y2 = self.y2; + let y3 = self.y3; + + let a = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3)) / ((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3)); + let b = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3)) / ((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3)); + let c = 1. - a - b; + + (0. <= a) & (a <= 1.) & (0. <= b) & (b <= 1.) & (0. <= c) & (c <= 1.) + } + + /// Returns a point (x, y) that is the centroid of the triangle. + pub fn centroid(&self) -> (f64, f64) { + ((self.x1 + self.x2 + self.x3)/3., (self.y1 + self.y2 + self.y3)/3.) + } + + /// Calculates the shortest distance from this triangle to the point (x, y). + pub fn distance_to(&self, x: f64, y: f64) -> f64 { + let mut distance_to = std::f64::MAX; + + for segment in &self.segments { + let length_2 = (segment.2 - segment.0)*(segment.2 - segment.0) + (segment.3 - segment.1)*(segment.3 - segment.1); + + assert!(length_2 != 0., "Geometry error: mesh contains triangle with zero-length side."); + + let u = ((x - segment.0)*(segment.2 - segment.0) + (y - segment.1)*(segment.3 - segment.1))/length_2; + + let distance = if u < 0. { + ((x - segment.0)*(x - segment.0) + (y - segment.1)*(y - segment.1)).sqrt() + } else if u > 1. { + ((x - segment.2)*(x - segment.2) + (y - segment.3)*(y - segment.3)).sqrt() + } else { + let x_intersect = segment.0 + u*(segment.2 - segment.0); + let y_intersect = segment.1 + u*(segment.3 - segment.1); + + ((x - x_intersect)*(x - x_intersect) + (y - y_intersect)*(y - y_intersect)).sqrt() + }; + + if distance < distance_to { + distance_to = distance; + } + } + distance_to + } + + /// Calculates the distance between the center of this triangle and the point (x, y). + pub fn distance_to_center(&self, x: f64, y: f64) -> f64 { + let centroid = self.centroid(); + ((x - centroid.0)*(x - centroid.0) + (y - centroid.1)*(y - centroid.1)).sqrt() + } +} diff --git a/src/input.rs b/src/input.rs index 561d03c..c682d58 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,407 +1,407 @@ -use super::*; - -pub trait InputFile: GeometryInput { - fn new(string: &str) -> Self; - fn get_options(&self) -> &Options; - fn get_material_parameters(&self) -> &material::MaterialParameters; - fn get_particle_parameters(&self) -> &particle::ParticleParameters; - fn get_geometry_input(&self) -> &::GeometryInput; -} - -pub trait GeometryInput { - type GeometryInput; -} - -/// Rustbca's internal representation of an input file. -#[derive(Deserialize, Clone)] -pub struct Input2D { - pub options: Options, - pub material_parameters: material::MaterialParameters, - pub particle_parameters: particle::ParticleParameters, - pub geometry_input: geometry::Mesh2DInput, -} - -impl GeometryInput for Input2D { - type GeometryInput = geometry::Mesh2DInput; -} - -impl InputFile for Input2D { - - fn new(string: &str) -> Input2D { - toml::from_str(string).unwrap() - } - - fn get_options(&self) -> &Options{ - &self.options - } - fn get_material_parameters(&self) -> &material::MaterialParameters{ - &self.material_parameters - } - fn get_particle_parameters(&self) -> &particle::ParticleParameters{ - &self.particle_parameters - } - fn get_geometry_input(&self) -> &Self::GeometryInput{ - &self.geometry_input - } -} - -#[derive(Deserialize, Clone)] -pub struct Input0D { - pub options: Options, - pub material_parameters: material::MaterialParameters, - pub particle_parameters: particle::ParticleParameters, - pub geometry_input: geometry::Mesh0DInput, -} - -impl GeometryInput for Input0D { - type GeometryInput = geometry::Mesh0DInput; -} - -impl InputFile for Input0D { - - fn new(string: &str) -> Input0D { - toml::from_str(string).expect("Could not parse TOML file.") - } - - fn get_options(&self) -> &Options{ - &self.options - } - fn get_material_parameters(&self) -> &material::MaterialParameters{ - &self.material_parameters - } - fn get_particle_parameters(&self) ->& particle::ParticleParameters{ - &self.particle_parameters - } - fn get_geometry_input(&self) -> &Self::GeometryInput{ - &self.geometry_input - } -} - -#[derive(Deserialize, Clone)] -pub struct Input1D { - pub options: Options, - pub material_parameters: material::MaterialParameters, - pub particle_parameters: particle::ParticleParameters, - pub geometry_input: geometry::Mesh1DInput, -} - -impl GeometryInput for Input1D { - type GeometryInput = geometry::Mesh1DInput; -} - -impl InputFile for Input1D { - - fn new(string: &str) -> Input1D { - toml::from_str(string).expect("Could not parse TOML file.") - } - - fn get_options(&self) -> &Options{ - &self.options - } - fn get_material_parameters(&self) -> &material::MaterialParameters{ - &self.material_parameters - } - fn get_particle_parameters(&self) ->& particle::ParticleParameters{ - &self.particle_parameters - } - fn get_geometry_input(&self) -> &Self::GeometryInput{ - &self.geometry_input - } -} - -/// Rustbca's internal representation of the simulation-level options. -#[cfg(not(feature = "distributions"))] -#[derive(Deserialize, Clone)] -pub struct Options { - pub name: String, - pub track_trajectories: bool, - pub track_recoils: bool, - pub track_recoil_trajectories: bool, - pub write_buffer_size: usize, - pub weak_collision_order: usize, - pub suppress_deep_recoils: bool, - pub high_energy_free_flight_paths: bool, - pub electronic_stopping_mode: ElectronicStoppingMode, - pub mean_free_path_model: MeanFreePathModel, - pub interaction_potential: Vec>, - pub scattering_integral: Vec>, - pub root_finder: Vec>, - pub num_threads: usize, - pub num_chunks: u64, - pub use_hdf5: bool, - pub track_displacements: bool, - pub track_energy_losses: bool, -} - -#[cfg(feature = "distributions")] -#[derive(Deserialize, Clone)] -pub struct Options { - pub name: String, - pub track_trajectories: bool, - pub track_recoils: bool, - pub track_recoil_trajectories: bool, - pub write_buffer_size: usize, - pub weak_collision_order: usize, - pub suppress_deep_recoils: bool, - pub high_energy_free_flight_paths: bool, - pub electronic_stopping_mode: ElectronicStoppingMode, - pub mean_free_path_model: MeanFreePathModel, - pub interaction_potential: Vec>, - pub scattering_integral: Vec>, - pub root_finder: Vec>, - pub num_threads: usize, - pub num_chunks: u64, - pub use_hdf5: bool, - pub track_displacements: bool, - pub track_energy_losses: bool, - pub energy_min: f64, - pub energy_max: f64, - pub energy_num: usize, - pub angle_min: f64, - pub angle_max: f64, - pub angle_num: usize, - pub x_min: f64, - pub y_min: f64, - pub z_min: f64, - pub x_max: f64, - pub y_max: f64, - pub z_max: f64, - pub x_num: usize, - pub y_num: usize, - pub z_num: usize, -} - -pub fn input(input_file: String) -> (Vec, material::Material, Options, OutputUnits) -where ::InputFileFormat: Deserialize<'static> + 'static { - - //Read input file, convert to string, and open with toml - let mut input_toml = String::new(); - let mut file = OpenOptions::new() - .read(true) - .write(false) - .create(false) - .open(&input_file) - .expect(format!("Input errror: could not open input file {}.", &input_file).as_str()); - file.read_to_string(&mut input_toml).context("Could not convert TOML file to string.").unwrap(); - - let input: ::InputFileFormat = InputFile::new(&input_toml); - - //Unpack toml information into structs - let options = (*input.get_options()).clone(); - let particle_parameters = (*input.get_particle_parameters()).clone(); - let material_parameters = (*input.get_material_parameters()).clone(); - let material: material::Material = material::Material::::new(&material_parameters, input.get_geometry_input()); - - //Ensure nonsensical threads/chunks options crash on input - assert!(options.num_threads > 0, "Input error: num_threads must be greater than zero."); - assert!(options.num_chunks > 0, "Input error: num_chunks must be greater than zero."); - - //Check that all material arrays are of equal length. - assert!(material.m.len() == material.Z.len(), "Input error: material input arrays of unequal length."); - assert!(material.m.len() == material.Eb.len(), "Input error: material input arrays of unequal length."); - assert!(material.m.len() == material.Es.len(), "Input error: material input arrays of unequal length."); - assert!(material.m.len() == material.interaction_index.len(), "Input error: material input arrays of unequal length."); - - //Check that incompatible options are not on simultaneously - if options.high_energy_free_flight_paths { - assert!(options.electronic_stopping_mode == ElectronicStoppingMode::INTERPOLATED, - "Input error: High energy free flight paths used with low energy stoppping power. Change to INTERPOLATED."); - assert!(options.weak_collision_order == 0, - "Input error: Cannot use weak collisions with free flight paths. Set weak_collision_order = 0."); - } - - if options.mean_free_path_model == MeanFreePathModel::GASEOUS { - assert!(options.weak_collision_order == 0, - "Input error: Cannot use weak collisions with gaseous mean free path model. Set weak_collision_order = 0."); - } - - for i in 0..options.interaction_potential.len() { - assert!(options.interaction_potential.len() == options.interaction_potential[i].len(), - "Input error: interaction matrix not square."); - - assert!(options.scattering_integral.len() == options.scattering_integral[i].len(), - "Input error: scattering intergral matrix not square."); - - assert!(options.root_finder.len() == options.root_finder[i].len(), - "Input error: rootfinder matrix not square."); - } - - for ((interaction_potentials, scattering_integrals), root_finders) in options.interaction_potential.iter().zip(options.scattering_integral.clone()).zip(options.root_finder.clone()) { - for ((interaction_potential, scattering_integral), root_finder) in interaction_potentials.iter().zip(scattering_integrals).zip(root_finders) { - - if cfg!(not(any(feature="cpr_rootfinder_openblas", feature="cpr_rootfinder_netlib", feature="cpr_rootfinder_intel_mkl",))) { - assert!( match root_finder { - Rootfinder::POLYNOMIAL{..} => false, - Rootfinder::CPR{..} => false, - _ => true, - }, - "Input error: CPR rootfinder not enabled. Build with --features cpr_rootfinder"); - } - - assert!( - match (interaction_potential, root_finder) { - (InteractionPotential::LENNARD_JONES_12_6{..}, Rootfinder::CPR{..}) => true, - (InteractionPotential::LENNARD_JONES_12_6{..}, Rootfinder::POLYNOMIAL{..}) => true, - (InteractionPotential::LENNARD_JONES_12_6{..}, _) => false, - (InteractionPotential::LENNARD_JONES_65_6{..}, Rootfinder::CPR{..}) => true, - (InteractionPotential::LENNARD_JONES_65_6{..}, Rootfinder::POLYNOMIAL{..}) => true, - (InteractionPotential::LENNARD_JONES_65_6{..}, _) => false, - (InteractionPotential::MORSE{..}, Rootfinder::CPR{..}) => true, - (InteractionPotential::MORSE{..}, _) => false, - (_, Rootfinder::POLYNOMIAL{..}) => false, - (_, _) => true, - }, - "Input error: cannot use {} with {}. Try switching to a different rootfinder.", interaction_potential, root_finder); - } - } - - //Check that particle arrays are equal length - assert_eq!(particle_parameters.Z.len(), particle_parameters.m.len(), - "Input error: particle input arrays of unequal length."); - assert_eq!(particle_parameters.Z.len(), particle_parameters.E.len(), - "Input error: particle input arrays of unequal length."); - assert_eq!(particle_parameters.Z.len(), particle_parameters.pos.len(), - "Input error: particle input arrays of unequal length."); - assert_eq!(particle_parameters.Z.len(), particle_parameters.dir.len(), - "Input error: particle input arrays of unequal length."); - - //Check that interaction indices are all within interaction matrices - assert!(material.interaction_index.iter().max().unwrap() < &options.interaction_potential.len(), - "Input error: interaction matrix too small for material interaction indices."); - assert!(particle_parameters.interaction_index.iter().max().unwrap() < &options.interaction_potential.len(), - "Input error: interaction matrix too small for particle interaction indices."); - - //N is the number of distinct particles. - let N = particle_parameters.Z.len(); - - //Determine the length, energy, and mass units for particle input - let length_unit: f64 = match particle_parameters.length_unit.as_str() { - "MICRON" => MICRON, - "CM" => CM, - "ANGSTROM" => ANGSTROM, - "NM" => NM, - "M" => 1., - _ => particle_parameters.length_unit.parse() - .expect(format!( - "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", &particle_parameters.length_unit.as_str() - ).as_str()), - }; - - let energy_unit: f64 = match particle_parameters.energy_unit.as_str() { - "EV" => EV, - "J" => 1., - "KEV" => EV*1E3, - "MEV" => EV*1E6, - _ => particle_parameters.energy_unit.parse() - .expect(format!( - "Input errror: could nor parse energy unit {}. Use a valid float or one of EV, J, KEV, MEV", &particle_parameters.energy_unit.as_str() - ).as_str()), - }; - - let mass_unit: f64 = match particle_parameters.mass_unit.as_str() { - "AMU" => AMU, - "KG" => 1.0, - _ => particle_parameters.mass_unit.parse() - .expect(format!( - "Input errror: could nor parse mass unit {}. Use a valid float or one of AMU, KG", &particle_parameters.mass_unit.as_str() - ).as_str()), - }; - - //HDF5 - #[cfg(feature = "hdf5_input")] - let particle_input_array: Vec = { - if options.use_hdf5 { - let particle_input_filename = particle_parameters.particle_input_filename.as_str(); - let _e = hdf5::silence_errors(); - let particle_input_file = hdf5::File::open(particle_input_filename) - .context("Input error: cannot open HDF5 file.") - .unwrap(); - let particle_input = particle_input_file.dataset("particles") - .context("Input error: cannot read from HDF5 file.") - .unwrap(); - particle_input.read_raw::().unwrap() - - } else { - let mut particle_input: Vec = Vec::new(); - - for particle_index in 0..N { - let N_ = particle_parameters.N[particle_index]; - let m = particle_parameters.m[particle_index]; - let Z = particle_parameters.Z[particle_index]; - let E = particle_parameters.E[particle_index]; - let Ec = particle_parameters.Ec[particle_index]; - let Es = particle_parameters.Es[particle_index]; - let interaction_index = particle_parameters.interaction_index[particle_index]; - let (x, y, z) = particle_parameters.pos[particle_index]; - let (cosx, cosy, cosz) = particle_parameters.dir[particle_index]; - assert!(cosx < 1., - "Input error: particle x-direction cannot be exactly equal to 1 to avoid numerical gimbal lock."); - for sub_particle_index in 0..N_ { - //Add new particle to particle vector - particle_input.push( - particle::ParticleInput{ - m: m*mass_unit, - Z: Z, - E: E*energy_unit, - Ec: Ec*energy_unit, - Es: Es*energy_unit, - x: x*length_unit, - y: y*length_unit, - z: z*length_unit, - ux: cosx, - uy: cosy, - uz: cosz, - interaction_index: interaction_index - } - ); - } - } - particle_input - } - }; - - #[cfg(not(feature = "hdf5_input"))] - let particle_input_array: Vec = { - if options.use_hdf5 { - panic!("HDF5 particle input not enabled. Enable with: cargo build --features hdf5_input") - } else { - let mut particle_input: Vec = Vec::new(); - - for particle_index in 0..N { - let N_ = particle_parameters.N[particle_index]; - let m = particle_parameters.m[particle_index]; - let Z = particle_parameters.Z[particle_index]; - let E = particle_parameters.E[particle_index]; - let Ec = particle_parameters.Ec[particle_index]; - let Es = particle_parameters.Es[particle_index]; - let interaction_index = particle_parameters.interaction_index[particle_index]; - let (x, y, z) = particle_parameters.pos[particle_index]; - let (cosx, cosy, cosz) = particle_parameters.dir[particle_index]; - assert!(cosx < 1., - "Input error: particle x-direction cannot be exactly equal to 1 to avoid numerical gimbal lock."); - for sub_particle_index in 0..N_ { - - //Add new particle to particle vector - particle_input.push( - particle::ParticleInput{ - m: m*mass_unit, - Z: Z, - E: E*energy_unit, - Ec: Ec*energy_unit, - Es: Es*energy_unit, - x: x*length_unit, - y: y*length_unit, - z: z*length_unit, - ux: cosx, - uy: cosy, - uz: cosz, - interaction_index - } - ); - } - } - particle_input - } - }; - (particle_input_array, material, options, OutputUnits {length_unit, energy_unit, mass_unit}) -} +use super::*; + +pub trait InputFile: GeometryInput { + fn new(string: &str) -> Self; + fn get_options(&self) -> &Options; + fn get_material_parameters(&self) -> &material::MaterialParameters; + fn get_particle_parameters(&self) -> &particle::ParticleParameters; + fn get_geometry_input(&self) -> &::GeometryInput; +} + +pub trait GeometryInput { + type GeometryInput; +} + +/// Rustbca's internal representation of an input file. +#[derive(Deserialize, Clone)] +pub struct Input2D { + pub options: Options, + pub material_parameters: material::MaterialParameters, + pub particle_parameters: particle::ParticleParameters, + pub geometry_input: geometry::Mesh2DInput, +} + +impl GeometryInput for Input2D { + type GeometryInput = geometry::Mesh2DInput; +} + +impl InputFile for Input2D { + + fn new(string: &str) -> Input2D { + toml::from_str(string).unwrap() + } + + fn get_options(&self) -> &Options{ + &self.options + } + fn get_material_parameters(&self) -> &material::MaterialParameters{ + &self.material_parameters + } + fn get_particle_parameters(&self) -> &particle::ParticleParameters{ + &self.particle_parameters + } + fn get_geometry_input(&self) -> &Self::GeometryInput{ + &self.geometry_input + } +} + +#[derive(Deserialize, Clone)] +pub struct Input0D { + pub options: Options, + pub material_parameters: material::MaterialParameters, + pub particle_parameters: particle::ParticleParameters, + pub geometry_input: geometry::Mesh0DInput, +} + +impl GeometryInput for Input0D { + type GeometryInput = geometry::Mesh0DInput; +} + +impl InputFile for Input0D { + + fn new(string: &str) -> Input0D { + toml::from_str(string).expect("Could not parse TOML file.") + } + + fn get_options(&self) -> &Options{ + &self.options + } + fn get_material_parameters(&self) -> &material::MaterialParameters{ + &self.material_parameters + } + fn get_particle_parameters(&self) ->& particle::ParticleParameters{ + &self.particle_parameters + } + fn get_geometry_input(&self) -> &Self::GeometryInput{ + &self.geometry_input + } +} + +#[derive(Deserialize, Clone)] +pub struct Input1D { + pub options: Options, + pub material_parameters: material::MaterialParameters, + pub particle_parameters: particle::ParticleParameters, + pub geometry_input: geometry::Mesh1DInput, +} + +impl GeometryInput for Input1D { + type GeometryInput = geometry::Mesh1DInput; +} + +impl InputFile for Input1D { + + fn new(string: &str) -> Input1D { + toml::from_str(string).expect("Could not parse TOML file.") + } + + fn get_options(&self) -> &Options{ + &self.options + } + fn get_material_parameters(&self) -> &material::MaterialParameters{ + &self.material_parameters + } + fn get_particle_parameters(&self) ->& particle::ParticleParameters{ + &self.particle_parameters + } + fn get_geometry_input(&self) -> &Self::GeometryInput{ + &self.geometry_input + } +} + +/// Rustbca's internal representation of the simulation-level options. +#[cfg(not(feature = "distributions"))] +#[derive(Deserialize, Clone)] +pub struct Options { + pub name: String, + pub track_trajectories: bool, + pub track_recoils: bool, + pub track_recoil_trajectories: bool, + pub write_buffer_size: usize, + pub weak_collision_order: usize, + pub suppress_deep_recoils: bool, + pub high_energy_free_flight_paths: bool, + pub electronic_stopping_mode: ElectronicStoppingMode, + pub mean_free_path_model: MeanFreePathModel, + pub interaction_potential: Vec>, + pub scattering_integral: Vec>, + pub root_finder: Vec>, + pub num_threads: usize, + pub num_chunks: u64, + pub use_hdf5: bool, + pub track_displacements: bool, + pub track_energy_losses: bool, +} + +#[cfg(feature = "distributions")] +#[derive(Deserialize, Clone)] +pub struct Options { + pub name: String, + pub track_trajectories: bool, + pub track_recoils: bool, + pub track_recoil_trajectories: bool, + pub write_buffer_size: usize, + pub weak_collision_order: usize, + pub suppress_deep_recoils: bool, + pub high_energy_free_flight_paths: bool, + pub electronic_stopping_mode: ElectronicStoppingMode, + pub mean_free_path_model: MeanFreePathModel, + pub interaction_potential: Vec>, + pub scattering_integral: Vec>, + pub root_finder: Vec>, + pub num_threads: usize, + pub num_chunks: u64, + pub use_hdf5: bool, + pub track_displacements: bool, + pub track_energy_losses: bool, + pub energy_min: f64, + pub energy_max: f64, + pub energy_num: usize, + pub angle_min: f64, + pub angle_max: f64, + pub angle_num: usize, + pub x_min: f64, + pub y_min: f64, + pub z_min: f64, + pub x_max: f64, + pub y_max: f64, + pub z_max: f64, + pub x_num: usize, + pub y_num: usize, + pub z_num: usize, +} + +pub fn input(input_file: String) -> (Vec, material::Material, Options, OutputUnits) +where ::InputFileFormat: Deserialize<'static> + 'static { + + //Read input file, convert to string, and open with toml + let mut input_toml = String::new(); + let mut file = OpenOptions::new() + .read(true) + .write(false) + .create(false) + .open(&input_file) + .expect(format!("Input errror: could not open input file {}.", &input_file).as_str()); + file.read_to_string(&mut input_toml).context("Could not convert TOML file to string.").unwrap(); + + let input: ::InputFileFormat = InputFile::new(&input_toml); + + //Unpack toml information into structs + let options = (*input.get_options()).clone(); + let particle_parameters = (*input.get_particle_parameters()).clone(); + let material_parameters = (*input.get_material_parameters()).clone(); + let material: material::Material = material::Material::::new(&material_parameters, input.get_geometry_input()); + + //Ensure nonsensical threads/chunks options crash on input + assert!(options.num_threads > 0, "Input error: num_threads must be greater than zero."); + assert!(options.num_chunks > 0, "Input error: num_chunks must be greater than zero."); + + //Check that all material arrays are of equal length. + assert!(material.m.len() == material.Z.len(), "Input error: material input arrays of unequal length."); + assert!(material.m.len() == material.Eb.len(), "Input error: material input arrays of unequal length."); + assert!(material.m.len() == material.Es.len(), "Input error: material input arrays of unequal length."); + assert!(material.m.len() == material.interaction_index.len(), "Input error: material input arrays of unequal length."); + + //Check that incompatible options are not on simultaneously + if options.high_energy_free_flight_paths { + assert!(options.electronic_stopping_mode == ElectronicStoppingMode::INTERPOLATED, + "Input error: High energy free flight paths used with low energy stoppping power. Change to INTERPOLATED."); + assert!(options.weak_collision_order == 0, + "Input error: Cannot use weak collisions with free flight paths. Set weak_collision_order = 0."); + } + + if options.mean_free_path_model == MeanFreePathModel::GASEOUS { + assert!(options.weak_collision_order == 0, + "Input error: Cannot use weak collisions with gaseous mean free path model. Set weak_collision_order = 0."); + } + + for i in 0..options.interaction_potential.len() { + assert!(options.interaction_potential.len() == options.interaction_potential[i].len(), + "Input error: interaction matrix not square."); + + assert!(options.scattering_integral.len() == options.scattering_integral[i].len(), + "Input error: scattering intergral matrix not square."); + + assert!(options.root_finder.len() == options.root_finder[i].len(), + "Input error: rootfinder matrix not square."); + } + + for ((interaction_potentials, scattering_integrals), root_finders) in options.interaction_potential.iter().zip(options.scattering_integral.clone()).zip(options.root_finder.clone()) { + for ((interaction_potential, scattering_integral), root_finder) in interaction_potentials.iter().zip(scattering_integrals).zip(root_finders) { + + if cfg!(not(any(feature="cpr_rootfinder_openblas", feature="cpr_rootfinder_netlib", feature="cpr_rootfinder_intel_mkl",))) { + assert!( match root_finder { + Rootfinder::POLYNOMIAL{..} => false, + Rootfinder::CPR{..} => false, + _ => true, + }, + "Input error: CPR rootfinder not enabled. Build with --features cpr_rootfinder"); + } + + assert!( + match (interaction_potential, root_finder) { + (InteractionPotential::LENNARD_JONES_12_6{..}, Rootfinder::CPR{..}) => true, + (InteractionPotential::LENNARD_JONES_12_6{..}, Rootfinder::POLYNOMIAL{..}) => true, + (InteractionPotential::LENNARD_JONES_12_6{..}, _) => false, + (InteractionPotential::LENNARD_JONES_65_6{..}, Rootfinder::CPR{..}) => true, + (InteractionPotential::LENNARD_JONES_65_6{..}, Rootfinder::POLYNOMIAL{..}) => true, + (InteractionPotential::LENNARD_JONES_65_6{..}, _) => false, + (InteractionPotential::MORSE{..}, Rootfinder::CPR{..}) => true, + (InteractionPotential::MORSE{..}, _) => false, + (_, Rootfinder::POLYNOMIAL{..}) => false, + (_, _) => true, + }, + "Input error: cannot use {} with {}. Try switching to a different rootfinder.", interaction_potential, root_finder); + } + } + + //Check that particle arrays are equal length + assert_eq!(particle_parameters.Z.len(), particle_parameters.m.len(), + "Input error: particle input arrays of unequal length."); + assert_eq!(particle_parameters.Z.len(), particle_parameters.E.len(), + "Input error: particle input arrays of unequal length."); + assert_eq!(particle_parameters.Z.len(), particle_parameters.pos.len(), + "Input error: particle input arrays of unequal length."); + assert_eq!(particle_parameters.Z.len(), particle_parameters.dir.len(), + "Input error: particle input arrays of unequal length."); + + //Check that interaction indices are all within interaction matrices + assert!(material.interaction_index.iter().max().unwrap() < &options.interaction_potential.len(), + "Input error: interaction matrix too small for material interaction indices."); + assert!(particle_parameters.interaction_index.iter().max().unwrap() < &options.interaction_potential.len(), + "Input error: interaction matrix too small for particle interaction indices."); + + //N is the number of distinct particles. + let N = particle_parameters.Z.len(); + + //Determine the length, energy, and mass units for particle input + let length_unit: f64 = match particle_parameters.length_unit.as_str() { + "MICRON" => MICRON, + "CM" => CM, + "ANGSTROM" => ANGSTROM, + "NM" => NM, + "M" => 1., + _ => particle_parameters.length_unit.parse() + .expect(format!( + "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", &particle_parameters.length_unit.as_str() + ).as_str()), + }; + + let energy_unit: f64 = match particle_parameters.energy_unit.as_str() { + "EV" => EV, + "J" => 1., + "KEV" => EV*1E3, + "MEV" => EV*1E6, + _ => particle_parameters.energy_unit.parse() + .expect(format!( + "Input errror: could nor parse energy unit {}. Use a valid float or one of EV, J, KEV, MEV", &particle_parameters.energy_unit.as_str() + ).as_str()), + }; + + let mass_unit: f64 = match particle_parameters.mass_unit.as_str() { + "AMU" => AMU, + "KG" => 1.0, + _ => particle_parameters.mass_unit.parse() + .expect(format!( + "Input errror: could nor parse mass unit {}. Use a valid float or one of AMU, KG", &particle_parameters.mass_unit.as_str() + ).as_str()), + }; + + //HDF5 + #[cfg(feature = "hdf5_input")] + let particle_input_array: Vec = { + if options.use_hdf5 { + let particle_input_filename = particle_parameters.particle_input_filename.as_str(); + let _e = hdf5::silence_errors(); + let particle_input_file = hdf5::File::open(particle_input_filename) + .context("Input error: cannot open HDF5 file.") + .unwrap(); + let particle_input = particle_input_file.dataset("particles") + .context("Input error: cannot read from HDF5 file.") + .unwrap(); + particle_input.read_raw::().unwrap() + + } else { + let mut particle_input: Vec = Vec::new(); + + for particle_index in 0..N { + let N_ = particle_parameters.N[particle_index]; + let m = particle_parameters.m[particle_index]; + let Z = particle_parameters.Z[particle_index]; + let E = particle_parameters.E[particle_index]; + let Ec = particle_parameters.Ec[particle_index]; + let Es = particle_parameters.Es[particle_index]; + let interaction_index = particle_parameters.interaction_index[particle_index]; + let (x, y, z) = particle_parameters.pos[particle_index]; + let (cosx, cosy, cosz) = particle_parameters.dir[particle_index]; + assert!(cosx < 1., + "Input error: particle x-direction cannot be exactly equal to 1 to avoid numerical gimbal lock."); + for sub_particle_index in 0..N_ { + //Add new particle to particle vector + particle_input.push( + particle::ParticleInput{ + m: m*mass_unit, + Z: Z, + E: E*energy_unit, + Ec: Ec*energy_unit, + Es: Es*energy_unit, + x: x*length_unit, + y: y*length_unit, + z: z*length_unit, + ux: cosx, + uy: cosy, + uz: cosz, + interaction_index: interaction_index + } + ); + } + } + particle_input + } + }; + + #[cfg(not(feature = "hdf5_input"))] + let particle_input_array: Vec = { + if options.use_hdf5 { + panic!("HDF5 particle input not enabled. Enable with: cargo build --features hdf5_input") + } else { + let mut particle_input: Vec = Vec::new(); + + for particle_index in 0..N { + let N_ = particle_parameters.N[particle_index]; + let m = particle_parameters.m[particle_index]; + let Z = particle_parameters.Z[particle_index]; + let E = particle_parameters.E[particle_index]; + let Ec = particle_parameters.Ec[particle_index]; + let Es = particle_parameters.Es[particle_index]; + let interaction_index = particle_parameters.interaction_index[particle_index]; + let (x, y, z) = particle_parameters.pos[particle_index]; + let (cosx, cosy, cosz) = particle_parameters.dir[particle_index]; + assert!(cosx < 1., + "Input error: particle x-direction cannot be exactly equal to 1 to avoid numerical gimbal lock."); + for sub_particle_index in 0..N_ { + + //Add new particle to particle vector + particle_input.push( + particle::ParticleInput{ + m: m*mass_unit, + Z: Z, + E: E*energy_unit, + Ec: Ec*energy_unit, + Es: Es*energy_unit, + x: x*length_unit, + y: y*length_unit, + z: z*length_unit, + ux: cosx, + uy: cosy, + uz: cosz, + interaction_index + } + ); + } + } + particle_input + } + }; + (particle_input_array, material, options, OutputUnits {length_unit, energy_unit, mass_unit}) +} diff --git a/src/interactions.rs b/src/interactions.rs index 9382f73..3c81cb7 100644 --- a/src/interactions.rs +++ b/src/interactions.rs @@ -1,441 +1,441 @@ -use super::*; - -/// Analytic solutions to outermost root of the interaction potential. -pub fn crossing_point_doca(interaction_potential: InteractionPotential) -> f64 { - - match interaction_potential { - InteractionPotential::LENNARD_JONES_12_6{sigma, ..} | InteractionPotential::LENNARD_JONES_65_6{sigma, ..} => sigma, - InteractionPotential::MORSE{D, alpha, r0} => (alpha*r0 - (2.0_f64).ln())/alpha, - InteractionPotential::WW => 50.*ANGSTROM, - _ => 10.*ANGSTROM, - //_ => panic!("Input error: potential never crosses zero for r > 0. Consider using the Newton rootfinder.") - } - -} - -/// Interaction potential between two particles a and b at a distance `r`. -pub fn interaction_potential(r: f64, a: f64, Za: f64, Zb: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { - screened_coulomb(r, a, Za, Zb, interaction_potential) - }, - InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { - lennard_jones(r, sigma, epsilon) - }, - InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { - lennard_jones_65_6(r, sigma, epsilon) - } - InteractionPotential::MORSE{D, alpha, r0} => { - morse(r, D, alpha, r0) - } - InteractionPotential::WW => { - tungsten_tungsten_cubic_spline(r) - } - InteractionPotential::COULOMB{Za, Zb} => { - coulomb(r, Za, Zb) - } - } -} - -/// Analytic energy threshold above which the distance of closest approach function is guaranteed to have only one root. -pub fn energy_threshold_single_root(interaction_potential: InteractionPotential) -> f64 { - match interaction_potential{ - InteractionPotential::LENNARD_JONES_12_6{..} | InteractionPotential::LENNARD_JONES_65_6{..} => f64::INFINITY, - InteractionPotential::MORSE{..} => f64::INFINITY, - InteractionPotential::WW => f64::INFINITY, - InteractionPotential::COULOMB{..} => f64::INFINITY, - InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => 0., - } -} - -/// Distance of closest approach function for screened coulomb potentials. -fn doca_function(x0: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { - //Nonlinear equation to determine distance of closest approach - return x0 - interactions::phi(x0, interaction_potential)/reduced_energy - beta*beta/x0; -} - -/// First derivative w.r.t. `r` of the distance of closest approach function for screened coulomb potentials. -fn diff_doca_function(x0: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { - //First differential of distance of closest approach function for N-R solver - return beta*beta/x0/x0 - interactions::dphi(x0, interaction_potential)/reduced_energy + 1. -} - -/// Singularity-free version of the screened-coulomb distance of closest approach function. -fn doca_function_transformed(x0: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { - //Singularity free version of doca function - return x0*x0 - x0*interactions::phi(x0, interaction_potential)/reduced_energy - beta*beta; -} - -/// First derivative w.r.t. `r` of the singularity-free version of the screened-coulomb distance of closest approach function. -fn diff_doca_function_transformed(x0: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { - //First differential of distance of closest approach function for N-R solver - return 2.*x0 - interactions::phi(x0, interaction_potential)/reduced_energy -} - -/// Distance of closest approach function. The outermost root of this function is the distance of closest approach, or classical turning point, which is the lower bound to the scattering integral. -pub fn distance_of_closest_approach_function(r: f64, a: f64, Za: f64, Zb: f64, relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a/Za/Zb*relative_energy; - let beta: f64 = impact_parameter/a; - doca_function(r/a, beta, reduced_energy, interaction_potential) - - }, - InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { - doca_lennard_jones(r, impact_parameter, relative_energy, sigma, epsilon) - }, - InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { - doca_lennard_jones_65_6(r, impact_parameter, relative_energy, sigma, epsilon) - }, - InteractionPotential::MORSE{D, alpha, r0} => { - doca_morse(r, impact_parameter, relative_energy, D, alpha, r0) - }, - InteractionPotential::WW => { - doca_tungsten_tungsten_cubic_spline(r, impact_parameter, relative_energy) - }, - InteractionPotential::COULOMB{..} => panic!("Coulombic potential cannot be used with rootfinder.") - } -} - -/// Singularity-free distance of closest approach function. The outermost root of this function is the distance of closest approach, or classical turning point, which is the lower bound to the scattering integral. -pub fn distance_of_closest_approach_function_singularity_free(r: f64, a: f64, Za: f64, Zb: f64, relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> f64 { - if r.is_nan() { - panic!("Numerical error: r is NaN in distance of closest approach function. Check Rootfinder.") - } - match interaction_potential { - InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a/Za/Zb*relative_energy; - let beta: f64 = impact_parameter/a; - doca_function_transformed(r/a, beta, reduced_energy, interaction_potential) - }, - InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { - doca_lennard_jones(r, impact_parameter, relative_energy, sigma, epsilon) - }, - InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { - doca_lennard_jones_65_6(r, impact_parameter, relative_energy, sigma, epsilon) - }, - InteractionPotential::MORSE{D, alpha, r0} => { - doca_morse(r, impact_parameter, relative_energy, D, alpha, r0) - }, - InteractionPotential::WW => { - doca_tungsten_tungsten_cubic_spline(r, impact_parameter, relative_energy) - }, - InteractionPotential::COULOMB{..} => panic!("Coulombic potential cannot be used with rootfinder.") - } -} - -/// Scaling function used to keep the distance of closest approach function ~1 for the CPR rootfinder. -pub fn scaling_function(r: f64, a: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { - 1./(1. + (r/a).powi(2)) - }, - InteractionPotential::LENNARD_JONES_12_6{sigma, ..} => { - let n = 11.; - 1./(1. + (r/sigma).powf(n)) - }, - InteractionPotential::LENNARD_JONES_65_6{sigma, ..} => { - let n = 6.; - 1./(1. + (r/sigma).powf(n)) - }, - InteractionPotential::MORSE{D, alpha, r0} => { - 1./(1. + (r*alpha).powi(2)) - } - InteractionPotential::WW => { - 1. - }, - InteractionPotential::COULOMB{..} => panic!("Coulombic potential cannot be used with rootfinder.") - } -} - -/// First derivative w.r.t. `r` of the distance of closest approach function. The outermost root of this function is the distance of closest approach, or classical turning point, which is the lower bound to the scattering integral. -pub fn diff_distance_of_closest_approach_function(r: f64, a: f64, Za: f64, Zb: f64, relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN |InteractionPotential::ZBL | InteractionPotential::TRIDYN => { - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - //let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; - let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a/Za/Zb*relative_energy; - let beta: f64 = impact_parameter/a; - diff_doca_function(r/a, beta, reduced_energy, interaction_potential) - }, - InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { - diff_doca_lennard_jones(r, impact_parameter, relative_energy, sigma, epsilon) - }, - InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { - diff_doca_lennard_jones_65_6(r, impact_parameter, relative_energy, sigma, epsilon) - }, - InteractionPotential::MORSE{D, alpha, r0} => { - diff_doca_morse(r, impact_parameter, relative_energy, D, alpha, r0) - }, - _ => panic!("Input error: {} does not have an implemented derivative. Try using the derivative free CPR-rootfinder.", interaction_potential) - } -} - -/// First derivative w.r.t. `r` of the singularity-free distance of closest approach function. The outermost root of this function is the distance of closest approach, or classical turning point, which is the lower bound to the scattering integral. -pub fn diff_distance_of_closest_approach_function_singularity_free(r: f64, a: f64, Za: f64, Zb: f64, relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { - let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); - //let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; - let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a/Za/Zb*relative_energy; - let beta: f64 = impact_parameter/a; - diff_doca_function_transformed(r/a, beta, reduced_energy, interaction_potential) - }, - InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { - diff_doca_lennard_jones(r, impact_parameter, relative_energy, sigma, epsilon) - }, - InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { - diff_doca_lennard_jones_65_6(r, impact_parameter, relative_energy, sigma, epsilon) - }, - InteractionPotential::MORSE{D, alpha, r0} => { - diff_doca_morse(r, impact_parameter, relative_energy, D, alpha, r0) - }, - _ => panic!("Input error: {} does not have an implemented derivative. Try using the derivative free CPR-rootfinder.", interaction_potential) - } -} - -/// Screened coulomb interaction potential. -pub fn screened_coulomb(r: f64, a: f64, Za: f64, Zb: f64, interaction_potential: InteractionPotential) -> f64 { - Za*Zb*Q*Q/4./PI/EPS0/r*phi(r/a, interaction_potential) -} - -/// Coulombic interaction potential. -pub fn coulomb(r: f64, Za: f64, Zb: f64) -> f64 { - return Za*Zb*Q*Q/4./PI/EPS0/r; -} - -/// Screening functions for screened-coulomb interaction potentials. -pub fn phi(xi: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::MOLIERE => moliere(xi), - InteractionPotential::KR_C => kr_c(xi), - InteractionPotential::ZBL => zbl(xi), - InteractionPotential::LENZ_JENSEN => lenz_jensen(xi), - InteractionPotential::TRIDYN => kr_c(xi), - _ => panic!("Input error: Screened Coulomb cannot be used with {}", interaction_potential) - } -} - -/// First derivative w.r.t. `r` of the screening functions for screened-coulomb interaction potentials. -pub fn dphi(xi: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::MOLIERE => diff_moliere(xi), - InteractionPotential::KR_C => diff_kr_c(xi), - InteractionPotential::ZBL => diff_zbl(xi), - InteractionPotential::LENZ_JENSEN => diff_lenz_jensen(xi), - InteractionPotential::TRIDYN => diff_kr_c(xi), - _ => panic!("Input error: Screened Coulomb cannot be used with {}", interaction_potential) - } -} - -/// Screening length of interatomic potentials. -pub fn screening_length(Za: f64, Zb: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - //ZBL screening length, Eckstein (4.1.8) - InteractionPotential::ZBL => 0.88534*A0/(Za.powf(0.23) + Zb.powf(0.23)), - //Lindhard/Firsov screening length, Eckstein (4.1.5) - InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::TRIDYN | InteractionPotential::WW => 0.8853*A0*(Za.sqrt() + Zb.sqrt()).powf(-2./3.), - InteractionPotential::LENNARD_JONES_12_6{..} | InteractionPotential::LENNARD_JONES_65_6{..} => 0.8853*A0*(Za.sqrt() + Zb.sqrt()).powf(-2./3.), - InteractionPotential::MORSE{D, alpha, r0} => alpha, - InteractionPotential::COULOMB{Za: Z1, Zb: Z2} => 0.88534*A0/(Z1.powf(0.23) + Z2.powf(0.23)), - } -} - -/// Coefficients of inverse-polynomial interaction potentials. -pub fn polynomial_coefficients(relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> Vec { - match interaction_potential { - InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { - vec![1., 0., -impact_parameter.powi(2), 0., 0., 0., 4.*epsilon*sigma.powf(6.)/relative_energy, 0., 0., 0., 0., 0., -4.*epsilon*sigma.powf(12.)/relative_energy] - }, - InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { - vec![1., 0., 0., 0., -impact_parameter.powi(2), 0., 0., 0., 0., 0., 0., 0., 4.*epsilon*sigma.powf(6.)/relative_energy, -4.*epsilon*sigma.powf(6.5)/relative_energy] - }, - _ => panic!("Input error: non-polynomial interaction potential used with polynomial root-finder.") - } -} - -/// Inverse-polynomial interaction potentials transformation to remove singularities for the CPR root-finder. -pub fn inverse_transform(x: f64, interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::LENNARD_JONES_12_6{..} => { - x - }, - InteractionPotential::LENNARD_JONES_65_6{..} => { - x*x - }, - _ => panic!("Input error: non-polynomial interaction potential used with polynomial root-finder transformation.") - } -} - -/// Lennard-Jones 12-6 -pub fn lennard_jones(r: f64, sigma: f64, epsilon: f64) -> f64 { - 4.*epsilon*((sigma/r).powf(12.) - (sigma/r).powf(6.)) -} - -/// Lennard-Jones 6.5-6 -pub fn lennard_jones_65_6(r: f64, sigma: f64, epsilon: f64) -> f64 { - 4.*epsilon*((sigma/r).powf(6.5) - (sigma/r).powf(6.)) -} - -/// Morse potential -pub fn morse(r: f64, D: f64, alpha: f64, r0: f64) -> f64 { - D*((-2.*alpha*(r - r0)).exp() - 2.*(-alpha*(r - r0)).exp()) -} - -/// Distance of closest approach function for Morse potential. -pub fn doca_morse(r: f64, impact_parameter: f64, relative_energy: f64, D: f64, alpha: f64, r0: f64) -> f64 { - (r*alpha).powi(2) - (r*alpha).powi(2)*D/relative_energy*((-2.*alpha*(r - r0)).exp() - 2.*(-alpha*(r - r0)).exp()) - (impact_parameter*alpha).powi(2) -} - -/// First derivative w.r.t. `r` of the distance of closest approach function for Morse potential. -pub fn diff_doca_morse(r: f64, impact_parameter: f64, relative_energy: f64, D: f64, alpha: f64, r0: f64) -> f64 { - 2.*alpha.powi(2)*r - 2.*alpha.powi(2)*D*r*(-2.*alpha*(r - r0) - 1.).exp()*(alpha*r*(alpha*(r - r0)).exp() - 2.*(alpha*(r - r0)).exp() - r*alpha + 1.) -} - -/// Distance of closest approach function for LJ 6.5-6 potential. -pub fn doca_lennard_jones_65_6(r: f64, p: f64, relative_energy: f64, sigma: f64, epsilon: f64) -> f64 { - (r/sigma).powf(6.5) - 4.*epsilon/relative_energy*(1. - (r/sigma).powf(0.5)) - (p/sigma).powi(2)*(r/sigma).powf(4.5) -} - -/// Distance of closest approach function for LJ 12-6 potential. -pub fn doca_lennard_jones(r: f64, p: f64, relative_energy: f64, sigma: f64, epsilon: f64) -> f64 { - (r/sigma).powf(12.) - 4.*epsilon/relative_energy*(1. - (r/sigma).powf(6.)) - p.powi(2)*r.powf(10.)/sigma.powf(12.) -} - -/// First derivative w.r.t. `r` of the distance of closest approach function for LJ 12-6 potential. -pub fn diff_doca_lennard_jones(r: f64, p: f64, relative_energy: f64, sigma: f64, epsilon: f64) -> f64 { - 12.*(r/sigma).powf(11.)/sigma + 4.*epsilon/relative_energy*6.*(r/sigma).powf(5.)/sigma - 10.*p.powi(2)*r.powf(9.)/sigma.powf(12.) -} - -/// First derivative w.r.t. `r` of the distance of closest approach function for LJ 6.5-6 potential. -pub fn diff_doca_lennard_jones_65_6(r: f64, p: f64, relative_energy: f64, sigma: f64, epsilon: f64) -> f64 { - 6.5*(r/sigma).powf(5.5)/sigma + 4.*epsilon/relative_energy*0.5*(sigma*r).powf(-0.5) - (p/sigma).powi(2)*4.5*(r/sigma).powf(3.5)/sigma -} - -/// W-W cublic spline potential from Bjorkas et al. -pub fn tungsten_tungsten_cubic_spline(r: f64) -> f64 { - - let x = r/ANGSTROM; - let x1 = 1.10002200044; - let x2 = 2.10004200084; - - if x <= x1 { - - let a = screening_length(74., 74., InteractionPotential::ZBL); - screened_coulomb(r, a, 74., 74., InteractionPotential::ZBL) - - } else if x <= x2 { - - let a = vec![ - 1.389653276380862E4, - -3.596912431628216E4, - 3.739206756369099E4, - -1.933748081656593E4, - 0.495516793802426E4, - -0.050264585985867E4 - ]; - - (a[0] + a[1]*x + a[2]*x.powi(2) + a[3]*x.powi(3) + a[4]*x.powf(4.) + a[5]*x.powf(5.))*EV - - } else { - - let a = vec![ - -0.1036435865158945, - -0.2912948318493851, - -2.096765499656263, - 19.16045452701010, - -41.01619862085917, - 46.05205617244703, - 26.42203930654883, - 15.35211507804088, - 14.12806259323987, - ]; - - let delta = vec![ - 4.268900000000000, - 3.985680000000000, - 3.702460000000000, - 3.419240000000000, - 3.136020000000000, - 2.852800000000000, - 2.741100000000000, - 2.604045000000000, - 2.466990000000000, - ]; - - a.iter().zip(delta).map(|(&a_i, delta_i)| EV*a_i*(delta_i - x).powi(3)*heaviside(delta_i - x)).sum::() - } -} - -/// Distance of closest approach function for the W-W cublic spline potential from Bjorkas et al. -pub fn doca_tungsten_tungsten_cubic_spline(r: f64, p: f64, relative_energy: f64) -> f64 { - - let x = r/ANGSTROM; - let x1 = 1.10002200044; - let x2 = 2.10004200084; - - if x <= x1 { - let a = screening_length(74., 74., InteractionPotential::ZBL); - distance_of_closest_approach_function_singularity_free(r, a, 74., 74., relative_energy, p, InteractionPotential::ZBL) - } else { - (r/ANGSTROM).powi(2) - (r/ANGSTROM).powi(2)*tungsten_tungsten_cubic_spline(r)/relative_energy - p.powi(2)/ANGSTROM.powi(2) - } -} - -fn heaviside(x: f64) -> f64 { - if x >= 0. { - 1. - } else { - 0. - } -} - -fn moliere(xi: f64) -> f64 { - 0.35*(-0.3*xi).exp() + 0.55*(-1.2*xi).exp() + 0.10*(-6.0*xi).exp() -} - -fn kr_c(xi: f64) -> f64 { - 0.190945*(-0.278544*xi).exp() + 0.473674*(-0.637174*xi).exp() + 0.335381*(-1.919249*xi).exp() -} - -fn zbl(xi: f64) -> f64 { - 0.02817*(-0.20162*xi).exp() + 0.28022*(-0.40290*xi).exp() + 0.50986*(-0.94229*xi).exp() + 0.18175*(-3.1998*xi).exp() -} - -fn lenz_jensen(xi: f64) -> f64 { - 0.01018*(-0.206*xi).exp() + 0.24330*(-0.3876*xi).exp() + 0.7466*(-1.038*xi).exp() -} - -fn diff_moliere(xi: f64) -> f64 { - -0.35*0.3*(-0.3*xi).exp() -0.55*1.2*(-1.2*xi).exp() -0.10*6.0*(-6.0*xi).exp() -} - -fn diff_kr_c(xi: f64) -> f64 { - -0.278544*0.190945*(-0.278544*xi).exp() - 0.637174*0.473674*(-0.637174*xi).exp() - 0.335381*1.919249*(-1.919249*xi).exp() -} - -fn diff_zbl(xi: f64) -> f64 { - -0.20162*0.02817*(-0.20162*xi).exp() -0.40290*0.28022*(-0.40290*xi).exp() -0.94229*0.50986*(-0.94229*xi).exp() -3.1998*0.18175*(-3.1998*xi).exp() -} - -fn diff_lenz_jensen(xi: f64) -> f64 { - -0.206*0.01018*(-0.206*xi).exp() -0.3876*0.24330*(-0.3876*xi).exp() -1.038*0.7466*(-1.038*xi).exp() -} - -/// Normalized first screening radius. Used in Oen-Robinson electronic stopping. -pub fn first_screening_radius(interaction_potential: InteractionPotential) -> f64 { - match interaction_potential { - InteractionPotential::MOLIERE => 0.3, - InteractionPotential::KR_C => 0.278544, - InteractionPotential::ZBL => 0.20162, - InteractionPotential::LENZ_JENSEN => 0.206, - InteractionPotential::TRIDYN => 0.278544, - InteractionPotential::LENNARD_JONES_12_6{..} => 1., - InteractionPotential::LENNARD_JONES_65_6{..} => 1., - InteractionPotential::MORSE{..} => 1., - InteractionPotential::WW => 1., - InteractionPotential::COULOMB{..} => 0.3, - } -} +use super::*; + +/// Analytic solutions to outermost root of the interaction potential. +pub fn crossing_point_doca(interaction_potential: InteractionPotential) -> f64 { + + match interaction_potential { + InteractionPotential::LENNARD_JONES_12_6{sigma, ..} | InteractionPotential::LENNARD_JONES_65_6{sigma, ..} => sigma, + InteractionPotential::MORSE{D, alpha, r0} => (alpha*r0 - (2.0_f64).ln())/alpha, + InteractionPotential::WW => 50.*ANGSTROM, + _ => 10.*ANGSTROM, + //_ => panic!("Input error: potential never crosses zero for r > 0. Consider using the Newton rootfinder.") + } + +} + +/// Interaction potential between two particles a and b at a distance `r`. +pub fn interaction_potential(r: f64, a: f64, Za: f64, Zb: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { + screened_coulomb(r, a, Za, Zb, interaction_potential) + }, + InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { + lennard_jones(r, sigma, epsilon) + }, + InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { + lennard_jones_65_6(r, sigma, epsilon) + } + InteractionPotential::MORSE{D, alpha, r0} => { + morse(r, D, alpha, r0) + } + InteractionPotential::WW => { + tungsten_tungsten_cubic_spline(r) + } + InteractionPotential::COULOMB{Za, Zb} => { + coulomb(r, Za, Zb) + } + } +} + +/// Analytic energy threshold above which the distance of closest approach function is guaranteed to have only one root. +pub fn energy_threshold_single_root(interaction_potential: InteractionPotential) -> f64 { + match interaction_potential{ + InteractionPotential::LENNARD_JONES_12_6{..} | InteractionPotential::LENNARD_JONES_65_6{..} => f64::INFINITY, + InteractionPotential::MORSE{..} => f64::INFINITY, + InteractionPotential::WW => f64::INFINITY, + InteractionPotential::COULOMB{..} => f64::INFINITY, + InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => 0., + } +} + +/// Distance of closest approach function for screened coulomb potentials. +fn doca_function(x0: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { + //Nonlinear equation to determine distance of closest approach + return x0 - interactions::phi(x0, interaction_potential)/reduced_energy - beta*beta/x0; +} + +/// First derivative w.r.t. `r` of the distance of closest approach function for screened coulomb potentials. +fn diff_doca_function(x0: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { + //First differential of distance of closest approach function for N-R solver + return beta*beta/x0/x0 - interactions::dphi(x0, interaction_potential)/reduced_energy + 1. +} + +/// Singularity-free version of the screened-coulomb distance of closest approach function. +fn doca_function_transformed(x0: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { + //Singularity free version of doca function + return x0*x0 - x0*interactions::phi(x0, interaction_potential)/reduced_energy - beta*beta; +} + +/// First derivative w.r.t. `r` of the singularity-free version of the screened-coulomb distance of closest approach function. +fn diff_doca_function_transformed(x0: f64, beta: f64, reduced_energy: f64, interaction_potential: InteractionPotential) -> f64 { + //First differential of distance of closest approach function for N-R solver + return 2.*x0 - interactions::phi(x0, interaction_potential)/reduced_energy +} + +/// Distance of closest approach function. The outermost root of this function is the distance of closest approach, or classical turning point, which is the lower bound to the scattering integral. +pub fn distance_of_closest_approach_function(r: f64, a: f64, Za: f64, Zb: f64, relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a/Za/Zb*relative_energy; + let beta: f64 = impact_parameter/a; + doca_function(r/a, beta, reduced_energy, interaction_potential) + + }, + InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { + doca_lennard_jones(r, impact_parameter, relative_energy, sigma, epsilon) + }, + InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { + doca_lennard_jones_65_6(r, impact_parameter, relative_energy, sigma, epsilon) + }, + InteractionPotential::MORSE{D, alpha, r0} => { + doca_morse(r, impact_parameter, relative_energy, D, alpha, r0) + }, + InteractionPotential::WW => { + doca_tungsten_tungsten_cubic_spline(r, impact_parameter, relative_energy) + }, + InteractionPotential::COULOMB{..} => panic!("Coulombic potential cannot be used with rootfinder.") + } +} + +/// Singularity-free distance of closest approach function. The outermost root of this function is the distance of closest approach, or classical turning point, which is the lower bound to the scattering integral. +pub fn distance_of_closest_approach_function_singularity_free(r: f64, a: f64, Za: f64, Zb: f64, relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> f64 { + if r.is_nan() { + panic!("Numerical error: r is NaN in distance of closest approach function. Check Rootfinder.") + } + match interaction_potential { + InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a/Za/Zb*relative_energy; + let beta: f64 = impact_parameter/a; + doca_function_transformed(r/a, beta, reduced_energy, interaction_potential) + }, + InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { + doca_lennard_jones(r, impact_parameter, relative_energy, sigma, epsilon) + }, + InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { + doca_lennard_jones_65_6(r, impact_parameter, relative_energy, sigma, epsilon) + }, + InteractionPotential::MORSE{D, alpha, r0} => { + doca_morse(r, impact_parameter, relative_energy, D, alpha, r0) + }, + InteractionPotential::WW => { + doca_tungsten_tungsten_cubic_spline(r, impact_parameter, relative_energy) + }, + InteractionPotential::COULOMB{..} => panic!("Coulombic potential cannot be used with rootfinder.") + } +} + +/// Scaling function used to keep the distance of closest approach function ~1 for the CPR rootfinder. +pub fn scaling_function(r: f64, a: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { + 1./(1. + (r/a).powi(2)) + }, + InteractionPotential::LENNARD_JONES_12_6{sigma, ..} => { + let n = 11.; + 1./(1. + (r/sigma).powf(n)) + }, + InteractionPotential::LENNARD_JONES_65_6{sigma, ..} => { + let n = 6.; + 1./(1. + (r/sigma).powf(n)) + }, + InteractionPotential::MORSE{D, alpha, r0} => { + 1./(1. + (r*alpha).powi(2)) + } + InteractionPotential::WW => { + 1. + }, + InteractionPotential::COULOMB{..} => panic!("Coulombic potential cannot be used with rootfinder.") + } +} + +/// First derivative w.r.t. `r` of the distance of closest approach function. The outermost root of this function is the distance of closest approach, or classical turning point, which is the lower bound to the scattering integral. +pub fn diff_distance_of_closest_approach_function(r: f64, a: f64, Za: f64, Zb: f64, relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN |InteractionPotential::ZBL | InteractionPotential::TRIDYN => { + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + //let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; + let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a/Za/Zb*relative_energy; + let beta: f64 = impact_parameter/a; + diff_doca_function(r/a, beta, reduced_energy, interaction_potential) + }, + InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { + diff_doca_lennard_jones(r, impact_parameter, relative_energy, sigma, epsilon) + }, + InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { + diff_doca_lennard_jones_65_6(r, impact_parameter, relative_energy, sigma, epsilon) + }, + InteractionPotential::MORSE{D, alpha, r0} => { + diff_doca_morse(r, impact_parameter, relative_energy, D, alpha, r0) + }, + _ => panic!("Input error: {} does not have an implemented derivative. Try using the derivative free CPR-rootfinder.", interaction_potential) + } +} + +/// First derivative w.r.t. `r` of the singularity-free distance of closest approach function. The outermost root of this function is the distance of closest approach, or classical turning point, which is the lower bound to the scattering integral. +pub fn diff_distance_of_closest_approach_function_singularity_free(r: f64, a: f64, Za: f64, Zb: f64, relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::ZBL | InteractionPotential::TRIDYN => { + let a: f64 = interactions::screening_length(Za, Zb, interaction_potential); + //let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a*Mb/(Ma+Mb)/Za/Zb*E0; + let reduced_energy: f64 = LINDHARD_REDUCED_ENERGY_PREFACTOR*a/Za/Zb*relative_energy; + let beta: f64 = impact_parameter/a; + diff_doca_function_transformed(r/a, beta, reduced_energy, interaction_potential) + }, + InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { + diff_doca_lennard_jones(r, impact_parameter, relative_energy, sigma, epsilon) + }, + InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { + diff_doca_lennard_jones_65_6(r, impact_parameter, relative_energy, sigma, epsilon) + }, + InteractionPotential::MORSE{D, alpha, r0} => { + diff_doca_morse(r, impact_parameter, relative_energy, D, alpha, r0) + }, + _ => panic!("Input error: {} does not have an implemented derivative. Try using the derivative free CPR-rootfinder.", interaction_potential) + } +} + +/// Screened coulomb interaction potential. +pub fn screened_coulomb(r: f64, a: f64, Za: f64, Zb: f64, interaction_potential: InteractionPotential) -> f64 { + Za*Zb*Q*Q/4./PI/EPS0/r*phi(r/a, interaction_potential) +} + +/// Coulombic interaction potential. +pub fn coulomb(r: f64, Za: f64, Zb: f64) -> f64 { + return Za*Zb*Q*Q/4./PI/EPS0/r; +} + +/// Screening functions for screened-coulomb interaction potentials. +pub fn phi(xi: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::MOLIERE => moliere(xi), + InteractionPotential::KR_C => kr_c(xi), + InteractionPotential::ZBL => zbl(xi), + InteractionPotential::LENZ_JENSEN => lenz_jensen(xi), + InteractionPotential::TRIDYN => kr_c(xi), + _ => panic!("Input error: Screened Coulomb cannot be used with {}", interaction_potential) + } +} + +/// First derivative w.r.t. `r` of the screening functions for screened-coulomb interaction potentials. +pub fn dphi(xi: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::MOLIERE => diff_moliere(xi), + InteractionPotential::KR_C => diff_kr_c(xi), + InteractionPotential::ZBL => diff_zbl(xi), + InteractionPotential::LENZ_JENSEN => diff_lenz_jensen(xi), + InteractionPotential::TRIDYN => diff_kr_c(xi), + _ => panic!("Input error: Screened Coulomb cannot be used with {}", interaction_potential) + } +} + +/// Screening length of interatomic potentials. +pub fn screening_length(Za: f64, Zb: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + //ZBL screening length, Eckstein (4.1.8) + InteractionPotential::ZBL => 0.88534*A0/(Za.powf(0.23) + Zb.powf(0.23)), + //Lindhard/Firsov screening length, Eckstein (4.1.5) + InteractionPotential::MOLIERE | InteractionPotential::KR_C | InteractionPotential::LENZ_JENSEN | InteractionPotential::TRIDYN | InteractionPotential::WW => 0.8853*A0*(Za.sqrt() + Zb.sqrt()).powf(-2./3.), + InteractionPotential::LENNARD_JONES_12_6{..} | InteractionPotential::LENNARD_JONES_65_6{..} => 0.8853*A0*(Za.sqrt() + Zb.sqrt()).powf(-2./3.), + InteractionPotential::MORSE{D, alpha, r0} => alpha, + InteractionPotential::COULOMB{Za: Z1, Zb: Z2} => 0.88534*A0/(Z1.powf(0.23) + Z2.powf(0.23)), + } +} + +/// Coefficients of inverse-polynomial interaction potentials. +pub fn polynomial_coefficients(relative_energy: f64, impact_parameter: f64, interaction_potential: InteractionPotential) -> Vec { + match interaction_potential { + InteractionPotential::LENNARD_JONES_12_6{sigma, epsilon} => { + vec![1., 0., -impact_parameter.powi(2), 0., 0., 0., 4.*epsilon*sigma.powf(6.)/relative_energy, 0., 0., 0., 0., 0., -4.*epsilon*sigma.powf(12.)/relative_energy] + }, + InteractionPotential::LENNARD_JONES_65_6{sigma, epsilon} => { + vec![1., 0., 0., 0., -impact_parameter.powi(2), 0., 0., 0., 0., 0., 0., 0., 4.*epsilon*sigma.powf(6.)/relative_energy, -4.*epsilon*sigma.powf(6.5)/relative_energy] + }, + _ => panic!("Input error: non-polynomial interaction potential used with polynomial root-finder.") + } +} + +/// Inverse-polynomial interaction potentials transformation to remove singularities for the CPR root-finder. +pub fn inverse_transform(x: f64, interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::LENNARD_JONES_12_6{..} => { + x + }, + InteractionPotential::LENNARD_JONES_65_6{..} => { + x*x + }, + _ => panic!("Input error: non-polynomial interaction potential used with polynomial root-finder transformation.") + } +} + +/// Lennard-Jones 12-6 +pub fn lennard_jones(r: f64, sigma: f64, epsilon: f64) -> f64 { + 4.*epsilon*((sigma/r).powf(12.) - (sigma/r).powf(6.)) +} + +/// Lennard-Jones 6.5-6 +pub fn lennard_jones_65_6(r: f64, sigma: f64, epsilon: f64) -> f64 { + 4.*epsilon*((sigma/r).powf(6.5) - (sigma/r).powf(6.)) +} + +/// Morse potential +pub fn morse(r: f64, D: f64, alpha: f64, r0: f64) -> f64 { + D*((-2.*alpha*(r - r0)).exp() - 2.*(-alpha*(r - r0)).exp()) +} + +/// Distance of closest approach function for Morse potential. +pub fn doca_morse(r: f64, impact_parameter: f64, relative_energy: f64, D: f64, alpha: f64, r0: f64) -> f64 { + (r*alpha).powi(2) - (r*alpha).powi(2)*D/relative_energy*((-2.*alpha*(r - r0)).exp() - 2.*(-alpha*(r - r0)).exp()) - (impact_parameter*alpha).powi(2) +} + +/// First derivative w.r.t. `r` of the distance of closest approach function for Morse potential. +pub fn diff_doca_morse(r: f64, impact_parameter: f64, relative_energy: f64, D: f64, alpha: f64, r0: f64) -> f64 { + 2.*alpha.powi(2)*r - 2.*alpha.powi(2)*D*r*(-2.*alpha*(r - r0) - 1.).exp()*(alpha*r*(alpha*(r - r0)).exp() - 2.*(alpha*(r - r0)).exp() - r*alpha + 1.) +} + +/// Distance of closest approach function for LJ 6.5-6 potential. +pub fn doca_lennard_jones_65_6(r: f64, p: f64, relative_energy: f64, sigma: f64, epsilon: f64) -> f64 { + (r/sigma).powf(6.5) - 4.*epsilon/relative_energy*(1. - (r/sigma).powf(0.5)) - (p/sigma).powi(2)*(r/sigma).powf(4.5) +} + +/// Distance of closest approach function for LJ 12-6 potential. +pub fn doca_lennard_jones(r: f64, p: f64, relative_energy: f64, sigma: f64, epsilon: f64) -> f64 { + (r/sigma).powf(12.) - 4.*epsilon/relative_energy*(1. - (r/sigma).powf(6.)) - p.powi(2)*r.powf(10.)/sigma.powf(12.) +} + +/// First derivative w.r.t. `r` of the distance of closest approach function for LJ 12-6 potential. +pub fn diff_doca_lennard_jones(r: f64, p: f64, relative_energy: f64, sigma: f64, epsilon: f64) -> f64 { + 12.*(r/sigma).powf(11.)/sigma + 4.*epsilon/relative_energy*6.*(r/sigma).powf(5.)/sigma - 10.*p.powi(2)*r.powf(9.)/sigma.powf(12.) +} + +/// First derivative w.r.t. `r` of the distance of closest approach function for LJ 6.5-6 potential. +pub fn diff_doca_lennard_jones_65_6(r: f64, p: f64, relative_energy: f64, sigma: f64, epsilon: f64) -> f64 { + 6.5*(r/sigma).powf(5.5)/sigma + 4.*epsilon/relative_energy*0.5*(sigma*r).powf(-0.5) - (p/sigma).powi(2)*4.5*(r/sigma).powf(3.5)/sigma +} + +/// W-W cublic spline potential from Bjorkas et al. +pub fn tungsten_tungsten_cubic_spline(r: f64) -> f64 { + + let x = r/ANGSTROM; + let x1 = 1.10002200044; + let x2 = 2.10004200084; + + if x <= x1 { + + let a = screening_length(74., 74., InteractionPotential::ZBL); + screened_coulomb(r, a, 74., 74., InteractionPotential::ZBL) + + } else if x <= x2 { + + let a = vec![ + 1.389653276380862E4, + -3.596912431628216E4, + 3.739206756369099E4, + -1.933748081656593E4, + 0.495516793802426E4, + -0.050264585985867E4 + ]; + + (a[0] + a[1]*x + a[2]*x.powi(2) + a[3]*x.powi(3) + a[4]*x.powf(4.) + a[5]*x.powf(5.))*EV + + } else { + + let a = vec![ + -0.1036435865158945, + -0.2912948318493851, + -2.096765499656263, + 19.16045452701010, + -41.01619862085917, + 46.05205617244703, + 26.42203930654883, + 15.35211507804088, + 14.12806259323987, + ]; + + let delta = vec![ + 4.268900000000000, + 3.985680000000000, + 3.702460000000000, + 3.419240000000000, + 3.136020000000000, + 2.852800000000000, + 2.741100000000000, + 2.604045000000000, + 2.466990000000000, + ]; + + a.iter().zip(delta).map(|(&a_i, delta_i)| EV*a_i*(delta_i - x).powi(3)*heaviside(delta_i - x)).sum::() + } +} + +/// Distance of closest approach function for the W-W cublic spline potential from Bjorkas et al. +pub fn doca_tungsten_tungsten_cubic_spline(r: f64, p: f64, relative_energy: f64) -> f64 { + + let x = r/ANGSTROM; + let x1 = 1.10002200044; + let x2 = 2.10004200084; + + if x <= x1 { + let a = screening_length(74., 74., InteractionPotential::ZBL); + distance_of_closest_approach_function_singularity_free(r, a, 74., 74., relative_energy, p, InteractionPotential::ZBL) + } else { + (r/ANGSTROM).powi(2) - (r/ANGSTROM).powi(2)*tungsten_tungsten_cubic_spline(r)/relative_energy - p.powi(2)/ANGSTROM.powi(2) + } +} + +fn heaviside(x: f64) -> f64 { + if x >= 0. { + 1. + } else { + 0. + } +} + +fn moliere(xi: f64) -> f64 { + 0.35*(-0.3*xi).exp() + 0.55*(-1.2*xi).exp() + 0.10*(-6.0*xi).exp() +} + +fn kr_c(xi: f64) -> f64 { + 0.190945*(-0.278544*xi).exp() + 0.473674*(-0.637174*xi).exp() + 0.335381*(-1.919249*xi).exp() +} + +fn zbl(xi: f64) -> f64 { + 0.02817*(-0.20162*xi).exp() + 0.28022*(-0.40290*xi).exp() + 0.50986*(-0.94229*xi).exp() + 0.18175*(-3.1998*xi).exp() +} + +fn lenz_jensen(xi: f64) -> f64 { + 0.01018*(-0.206*xi).exp() + 0.24330*(-0.3876*xi).exp() + 0.7466*(-1.038*xi).exp() +} + +fn diff_moliere(xi: f64) -> f64 { + -0.35*0.3*(-0.3*xi).exp() -0.55*1.2*(-1.2*xi).exp() -0.10*6.0*(-6.0*xi).exp() +} + +fn diff_kr_c(xi: f64) -> f64 { + -0.278544*0.190945*(-0.278544*xi).exp() - 0.637174*0.473674*(-0.637174*xi).exp() - 0.335381*1.919249*(-1.919249*xi).exp() +} + +fn diff_zbl(xi: f64) -> f64 { + -0.20162*0.02817*(-0.20162*xi).exp() -0.40290*0.28022*(-0.40290*xi).exp() -0.94229*0.50986*(-0.94229*xi).exp() -3.1998*0.18175*(-3.1998*xi).exp() +} + +fn diff_lenz_jensen(xi: f64) -> f64 { + -0.206*0.01018*(-0.206*xi).exp() -0.3876*0.24330*(-0.3876*xi).exp() -1.038*0.7466*(-1.038*xi).exp() +} + +/// Normalized first screening radius. Used in Oen-Robinson electronic stopping. +pub fn first_screening_radius(interaction_potential: InteractionPotential) -> f64 { + match interaction_potential { + InteractionPotential::MOLIERE => 0.3, + InteractionPotential::KR_C => 0.278544, + InteractionPotential::ZBL => 0.20162, + InteractionPotential::LENZ_JENSEN => 0.206, + InteractionPotential::TRIDYN => 0.278544, + InteractionPotential::LENNARD_JONES_12_6{..} => 1., + InteractionPotential::LENNARD_JONES_65_6{..} => 1., + InteractionPotential::MORSE{..} => 1., + InteractionPotential::WW => 1., + InteractionPotential::COULOMB{..} => 0.3, + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a851fea --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,222 @@ +#![allow(unused_variables)] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +#[cfg(feature = "cpr_rootfinder_openblas")] +extern crate openblas_src; +#[cfg(feature = "cpr_rootfinder_netlib")] +extern crate netlib_src; +#[cfg(feature = "cpr_rootfinder_intel_mkl")] +extern crate intel_mkl_src; + +use std::{fmt}; +use std::mem::discriminant; + +//Error handling crate +use anyhow::Result; +use anyhow::*; + +//Serializing/Deserializing crate +use serde::*; + +//Array input via hdf5 +#[cfg(feature = "hdf5_input")] +use hdf5::*; + +//I/O +use std::fs::OpenOptions; +use std::io::prelude::*; +use std::io::BufWriter; + +//itertools +use itertools::izip; + +//Math +use std::f64::consts::FRAC_2_SQRT_PI; +use std::f64::consts::PI; +use std::f64::consts::SQRT_2; + +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +//Load internal modules +pub mod material; +pub mod particle; +pub mod tests; +pub mod interactions; +pub mod bca; +pub mod geometry; +pub mod input; +pub mod output; +pub mod enums; +pub mod consts; +pub mod structs; +pub mod sphere; + +#[cfg(feature = "parry3d")] +pub mod parry; + +pub use crate::enums::*; +pub use crate::consts::*; +pub use crate::structs::*; +pub use crate::input::{Input2D, Input1D, Input0D, Options, InputFile, GeometryInput}; +pub use crate::output::{OutputUnits}; +pub use crate::geometry::{Geometry, GeometryElement, Mesh0D, Mesh1D, Mesh2D}; +pub use crate::sphere::{Sphere, SphereInput, InputSphere}; + +#[cfg(feature = "parry3d")] +pub use crate::parry::{ParryBall, ParryBallInput, InputParryBall, ParryTriMesh, ParryTriMeshInput, InputParryTriMesh}; + +#[pymodule] +pub fn pybca(py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(simple_bca_py, m)?)?; + Ok(()) +} + +#[repr(C)] +pub struct OutputBCA { + len: usize, + pub particles: *mut [f64; 9], +} + +#[no_mangle] +pub extern "C" fn simple_bca_c(x: f64, y: f64, z: f64, ux: f64, uy: f64, uz: f64, E1: f64, Z1: f64, m1: f64, Ec1: f64, Es1: f64, Z2: f64, m2: f64, Ec2: f64, Es2: f64, n2: f64, Eb2: f64) -> OutputBCA { + let mut output = simple_bca(x, y, z, ux, uy, uz, E1, Z1, m1, Ec1, Es1, Z2, m2, Ec2, Es2, n2, Eb2); + let len = output.len(); + let particles = output.as_mut_ptr(); + + std::mem::forget(output); + OutputBCA { + len, + particles + } +} + +#[pyfunction] +pub fn simple_bca_py(x: f64, y: f64, z: f64, ux: f64, uy: f64, uz: f64, E1: f64, Z1: f64, m1: f64, Ec1: f64, Es1: f64, Z2: f64, m2: f64, Ec2: f64, Es2: f64, n2: f64, Eb2: f64) -> Vec<[f64; 9]> { + simple_bca(x, y, z, ux, uy, uz, E1, Z1, m1, Ec1, Es1, Z2, m2, Ec2, Es2, n2, Eb2) +} + +pub fn simple_bca(x: f64, y: f64, z: f64, ux: f64, uy: f64, uz: f64, E1: f64, Z1: f64, m1: f64, Ec1: f64, Es1: f64, Z2: f64, m2: f64, Ec2: f64, Es2: f64, n2: f64, Eb2: f64) -> Vec<[f64; 9]> { + + #[cfg(feature = "distributions")] + let options = Options { + name: "test".to_string(), + track_trajectories: false, + track_recoils: true, + track_recoil_trajectories: false, + write_buffer_size: 8000, + weak_collision_order: 3, + suppress_deep_recoils: false, + high_energy_free_flight_paths: false, + electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, + mean_free_path_model: MeanFreePathModel::LIQUID, + interaction_potential: vec![vec![InteractionPotential::KR_C]], + scattering_integral: vec![vec![ScatteringIntegral::MENDENHALL_WELLER]], + num_threads: 1, + num_chunks: 1, + use_hdf5: false, + root_finder: vec![vec![Rootfinder::DEFAULTNEWTON]], + track_displacements: false, + track_energy_losses: false, + energy_min: 0.0, + energy_max: 10.0, + energy_num: 11, + angle_min: 0.0, + angle_max: 90.0, + angle_num: 11, + x_min: 0.0, + y_min: -10.0, + z_min: -10.0, + x_max: 10.0, + y_max: 10.0, + z_max: 10.0, + x_num: 11, + y_num: 11, + z_num: 11, + }; + + #[cfg(not(feature = "distributions"))] + let options = Options { + name: "test".to_string(), + track_trajectories: false, + track_recoils: true, + track_recoil_trajectories: false, + write_buffer_size: 8000, + weak_collision_order: 3, + suppress_deep_recoils: false, + high_energy_free_flight_paths: false, + electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, + mean_free_path_model: MeanFreePathModel::LIQUID, + interaction_potential: vec![vec![InteractionPotential::KR_C]], + scattering_integral: vec![vec![ScatteringIntegral::MENDENHALL_WELLER]], + num_threads: 1, + num_chunks: 1, + use_hdf5: false, + root_finder: vec![vec![Rootfinder::DEFAULTNEWTON]], + track_displacements: false, + track_energy_losses: false, + }; + + let p = particle::Particle { + m: m1*AMU, + Z: Z1, + E: E1*EV, + Ec: Ec1*EV, + Es: Es1*EV, + pos: Vector::new(x, y, z), + dir: Vector::new(ux, uy, uz), + pos_origin: Vector::new(x, y, z), + pos_old: Vector::new(x, y, z), + dir_old: Vector::new(ux, uy, uz), + energy_origin: E1*EV, + asymptotic_deflection: 0.0, + stopped: false, + left: false, + incident: true, + first_step: true, + trajectory: vec![], + energies: vec![], + track_trajectories: false, + number_collision_events: 0, + backreflected: false, + interaction_index : 0 + }; + + let material_parameters = material::MaterialParameters { + energy_unit: "EV".to_string(), + mass_unit: "AMU".to_string(), + Eb: vec![Eb2], + Es: vec![Es2], + Ec: vec![Ec2], + Z: vec![Z2], + m: vec![m2], + interaction_index: vec![0], + surface_binding_model: SurfaceBindingModel::AVERAGE, + bulk_binding_model: BulkBindingModel::INDIVIDUAL, + }; + + let geometry_input = geometry::Mesh0DInput { + length_unit: "ANGSTROM".to_string(), + densities: vec![n2], + electronic_stopping_correction_factor: 1.0 + }; + + let m = material::Material::::new(&material_parameters, &geometry_input); + + let output = bca::single_ion_bca(p, &m, &options); + + output.iter().filter(|particle| (particle.incident) | (particle.left)).map(|particle| + [ + particle.Z, + particle.m/AMU, + particle.E/EV, + particle.pos.x/ANGSTROM, + particle.pos.y/ANGSTROM, + particle.pos.z/ANGSTROM, + particle.dir.x, + particle.dir.y, + particle.dir.z + ] + ).collect() +} diff --git a/src/main.rs b/src/main.rs index c6d576d..e8b6d5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,205 +1,205 @@ -#![allow(unused_variables)] -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] - -#[cfg(feature = "cpr_rootfinder_openblas")] -extern crate openblas_src; -#[cfg(feature = "cpr_rootfinder_netlib")] -extern crate netlib_src; -#[cfg(feature = "cpr_rootfinder_intel_mkl")] -extern crate intel_mkl_src; - -use std::{env, fmt}; -use std::mem::discriminant; - -//Progress bar crate - works with rayon -use indicatif::{ProgressBar, ProgressStyle}; - -//Error handling crate -use anyhow::Result; -use anyhow::*; - -//Serializing/Deserializing crate -use serde::*; - -//Array input via hdf5 -#[cfg(feature = "hdf5_input")] -use hdf5::*; - -//Parallelization -use rayon::prelude::*; -use rayon::*; - -//I/O -use std::fs::OpenOptions; -use std::io::prelude::*; -use std::io::BufWriter; - -//itertools -use itertools::izip; - -//Math -use std::f64::consts::FRAC_2_SQRT_PI; -use std::f64::consts::PI; -use std::f64::consts::SQRT_2; - -//rng -//use rand::{Rng, thread_rng}; - -//Load internal modules -pub mod material; -pub mod particle; -pub mod tests; -pub mod interactions; -pub mod bca; -pub mod geometry; -pub mod input; -pub mod output; -pub mod enums; -pub mod consts; -pub mod structs; -pub mod sphere; - -#[cfg(feature = "parry3d")] -pub mod parry; - -pub use crate::enums::*; -pub use crate::consts::*; -pub use crate::structs::*; -pub use crate::input::{Input2D, Input1D, Input0D, Options, InputFile, GeometryInput}; -pub use crate::output::{OutputUnits}; -pub use crate::geometry::{Geometry, GeometryElement, Mesh0D, Mesh1D, Mesh2D}; -pub use crate::sphere::{Sphere, SphereInput, InputSphere}; - -#[cfg(feature = "parry3d")] -pub use crate::parry::{ParryBall, ParryBallInput, InputParryBall, ParryTriMesh, ParryTriMeshInput, InputParryTriMesh}; - -fn physics_loop(particle_input_array: Vec, material: material::Material, options: Options, output_units: OutputUnits) { - - println!("Processing {} ions...", particle_input_array.len()); - - let total_count: u64 = particle_input_array.len() as u64; - assert!(total_count/options.num_chunks > 0, "Input error: chunk size == 0 - reduce num_chunks or increase particle count."); - - #[cfg(not(feature = "no_list_output"))] - let mut output_list_streams = output::open_output_lists(&options); - - let mut summary = output::SummaryPerSpecies::new(&options); - - #[cfg(feature = "distributions")] - let mut distributions = output::Distributions::new(&options); - - //Initialize threads with rayon - println!("Initializing with {} threads...", options.num_threads); - if options.num_threads > 1 {let pool = rayon::ThreadPoolBuilder::new().num_threads(options.num_threads).build_global().unwrap();}; - - //Create and configure progress bar - let bar: ProgressBar = ProgressBar::new(total_count); - bar.set_style(ProgressStyle::default_bar() - .template("[{elapsed_precise}][{bar:40.cyan/blue}][{eta_precise}] {percent}%") - .progress_chars("#>-")); - - //Main loop - for (chunk_index, particle_input_chunk) in particle_input_array.chunks((total_count/options.num_chunks) as usize).enumerate() { - - let mut finished_particles: Vec = Vec::new(); - - if options.num_threads > 1 { - // BCA loop is implemented as parallelized extension of a per-chunk initially empty - // finished particle array via map from particle -> finished particles via BCA - finished_particles.par_extend( - particle_input_chunk.into_par_iter() - .map(|particle_input| { - bar.tick(); - bar.inc(1); - bca::single_ion_bca(particle::Particle::from_input(*particle_input, &options), &material, &options) - }).flatten() - ); - } else { - finished_particles.extend( - particle_input_chunk.iter() - .map(|particle_input| { - bar.tick(); - bar.inc(1); - bca::single_ion_bca(particle::Particle::from_input(*particle_input, &options), &material, &options) - }).flatten() - ); - } - bar.finish(); - - // Process this chunk of finished particles for output - for particle in finished_particles { - - summary.update(&particle); - - #[cfg(feature = "distributions")] - distributions.update(&particle, &output_units, &options, total_count as usize); - - #[cfg(not(feature = "no_list_output"))] - output::output_lists(&mut output_list_streams, particle, &options, &output_units); - - } - //Flush all file streams before dropping to ensure all data is written - #[cfg(not(feature = "no_list_output"))] - output::output_list_flush(&mut output_list_streams); - } - - summary.print(&options, &output_units); - - //Write distributions to file - #[cfg(feature = "distributions")] - distributions.print(&options); - - println!("Finished!"); -} - -fn main() { - - let args: Vec = env::args().collect(); - - let (input_file, geometry_type) = match args.len() { - 1 => ("input.toml".to_string(), GeometryType::MESH2D), - 2 => (args[1].clone(), GeometryType::MESH2D), - 3 => (args[2].clone(), match args[1].as_str() { - "0D" => GeometryType::MESH0D, - "1D" => GeometryType::MESH1D, - "2D" => GeometryType::MESH2D, - "SPHERE" => GeometryType::SPHERE, - #[cfg(feature = "parry3d")] - "BALL" => GeometryType::BALL, - #[cfg(feature = "parry3d")] - "TRIMESH" => GeometryType::TRIMESH, - _ => panic!("Unimplemented geometry {}.", args[1].clone()) - }), - _ => panic!("Too many command line arguments. RustBCA accepts 0 (use 'input.toml') 1 () or 2 ( )"), - }; - - match geometry_type { - GeometryType::MESH0D => { - let (particle_input_array, material, options, output_units) = input::input::(input_file); - physics_loop::(particle_input_array, material, options, output_units); - }, - GeometryType::MESH1D => { - let (particle_input_array, material, options, output_units) = input::input::(input_file); - physics_loop::(particle_input_array, material, options, output_units); - }, - GeometryType::MESH2D => { - let (particle_input_array, material, options, output_units) = input::input::(input_file); - physics_loop::(particle_input_array, material, options, output_units); - }, - GeometryType::SPHERE => { - let (particle_input_array, material, options, output_units) = input::input::(input_file); - physics_loop::(particle_input_array, material, options, output_units); - }, - #[cfg(feature = "parry3d")] - GeometryType::BALL => { - let (particle_input_array, material, options, output_units) = input::input::(input_file); - physics_loop::(particle_input_array, material, options, output_units); - } - #[cfg(feature = "parry3d")] - GeometryType::TRIMESH => { - let (particle_input_array, material, options, output_units) = input::input::(input_file); - physics_loop::(particle_input_array, material, options, output_units); - } - } -} +#![allow(unused_variables)] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +#[cfg(feature = "cpr_rootfinder_openblas")] +extern crate openblas_src; +#[cfg(feature = "cpr_rootfinder_netlib")] +extern crate netlib_src; +#[cfg(feature = "cpr_rootfinder_intel_mkl")] +extern crate intel_mkl_src; + +use std::{env, fmt}; +use std::mem::discriminant; + +//Progress bar crate - works with rayon +use indicatif::{ProgressBar, ProgressStyle}; + +//Error handling crate +use anyhow::Result; +use anyhow::*; + +//Serializing/Deserializing crate +use serde::*; + +//Array input via hdf5 +#[cfg(feature = "hdf5_input")] +use hdf5::*; + +//Parallelization +use rayon::prelude::*; +use rayon::*; + +//I/O +use std::fs::OpenOptions; +use std::io::prelude::*; +use std::io::BufWriter; + +//itertools +use itertools::izip; + +//Math +use std::f64::consts::FRAC_2_SQRT_PI; +use std::f64::consts::PI; +use std::f64::consts::SQRT_2; + +//rng +//use rand::{Rng, thread_rng}; + +//Load internal modules +pub mod material; +pub mod particle; +pub mod tests; +pub mod interactions; +pub mod bca; +pub mod geometry; +pub mod input; +pub mod output; +pub mod enums; +pub mod consts; +pub mod structs; +pub mod sphere; + +#[cfg(feature = "parry3d")] +pub mod parry; + +pub use crate::enums::*; +pub use crate::consts::*; +pub use crate::structs::*; +pub use crate::input::{Input2D, Input1D, Input0D, Options, InputFile, GeometryInput}; +pub use crate::output::{OutputUnits}; +pub use crate::geometry::{Geometry, GeometryElement, Mesh0D, Mesh1D, Mesh2D}; +pub use crate::sphere::{Sphere, SphereInput, InputSphere}; + +#[cfg(feature = "parry3d")] +pub use crate::parry::{ParryBall, ParryBallInput, InputParryBall, ParryTriMesh, ParryTriMeshInput, InputParryTriMesh}; + +fn physics_loop(particle_input_array: Vec, material: material::Material, options: Options, output_units: OutputUnits) { + + println!("Processing {} ions...", particle_input_array.len()); + + let total_count: u64 = particle_input_array.len() as u64; + assert!(total_count/options.num_chunks > 0, "Input error: chunk size == 0 - reduce num_chunks or increase particle count."); + + #[cfg(not(feature = "no_list_output"))] + let mut output_list_streams = output::open_output_lists(&options); + + let mut summary = output::SummaryPerSpecies::new(&options); + + #[cfg(feature = "distributions")] + let mut distributions = output::Distributions::new(&options); + + //Initialize threads with rayon + println!("Initializing with {} threads...", options.num_threads); + if options.num_threads > 1 {let pool = rayon::ThreadPoolBuilder::new().num_threads(options.num_threads).build_global().unwrap();}; + + //Create and configure progress bar + let bar: ProgressBar = ProgressBar::new(total_count); + bar.set_style(ProgressStyle::default_bar() + .template("[{elapsed_precise}][{bar:40.cyan/blue}][{eta_precise}] {percent}%") + .progress_chars("#>-")); + + //Main loop + for (chunk_index, particle_input_chunk) in particle_input_array.chunks((total_count/options.num_chunks) as usize).enumerate() { + + let mut finished_particles: Vec = Vec::new(); + + if options.num_threads > 1 { + // BCA loop is implemented as parallelized extension of a per-chunk initially empty + // finished particle array via map from particle -> finished particles via BCA + finished_particles.par_extend( + particle_input_chunk.into_par_iter() + .map(|particle_input| { + bar.tick(); + bar.inc(1); + bca::single_ion_bca(particle::Particle::from_input(*particle_input, &options), &material, &options) + }).flatten() + ); + } else { + finished_particles.extend( + particle_input_chunk.iter() + .map(|particle_input| { + bar.tick(); + bar.inc(1); + bca::single_ion_bca(particle::Particle::from_input(*particle_input, &options), &material, &options) + }).flatten() + ); + } + bar.finish(); + + // Process this chunk of finished particles for output + for particle in finished_particles { + + summary.update(&particle); + + #[cfg(feature = "distributions")] + distributions.update(&particle, &output_units, &options, total_count as usize); + + #[cfg(not(feature = "no_list_output"))] + output::output_lists(&mut output_list_streams, particle, &options, &output_units); + + } + //Flush all file streams before dropping to ensure all data is written + #[cfg(not(feature = "no_list_output"))] + output::output_list_flush(&mut output_list_streams); + } + + summary.print(&options, &output_units); + + //Write distributions to file + #[cfg(feature = "distributions")] + distributions.print(&options); + + println!("Finished!"); +} + +fn main() { + + let args: Vec = env::args().collect(); + + let (input_file, geometry_type) = match args.len() { + 1 => ("input.toml".to_string(), GeometryType::MESH2D), + 2 => (args[1].clone(), GeometryType::MESH2D), + 3 => (args[2].clone(), match args[1].as_str() { + "0D" => GeometryType::MESH0D, + "1D" => GeometryType::MESH1D, + "2D" => GeometryType::MESH2D, + "SPHERE" => GeometryType::SPHERE, + #[cfg(feature = "parry3d")] + "BALL" => GeometryType::BALL, + #[cfg(feature = "parry3d")] + "TRIMESH" => GeometryType::TRIMESH, + _ => panic!("Unimplemented geometry {}.", args[1].clone()) + }), + _ => panic!("Too many command line arguments. RustBCA accepts 0 (use 'input.toml') 1 () or 2 ( )"), + }; + + match geometry_type { + GeometryType::MESH0D => { + let (particle_input_array, material, options, output_units) = input::input::(input_file); + physics_loop::(particle_input_array, material, options, output_units); + }, + GeometryType::MESH1D => { + let (particle_input_array, material, options, output_units) = input::input::(input_file); + physics_loop::(particle_input_array, material, options, output_units); + }, + GeometryType::MESH2D => { + let (particle_input_array, material, options, output_units) = input::input::(input_file); + physics_loop::(particle_input_array, material, options, output_units); + }, + GeometryType::SPHERE => { + let (particle_input_array, material, options, output_units) = input::input::(input_file); + physics_loop::(particle_input_array, material, options, output_units); + }, + #[cfg(feature = "parry3d")] + GeometryType::BALL => { + let (particle_input_array, material, options, output_units) = input::input::(input_file); + physics_loop::(particle_input_array, material, options, output_units); + } + #[cfg(feature = "parry3d")] + GeometryType::TRIMESH => { + let (particle_input_array, material, options, output_units) = input::input::(input_file); + physics_loop::(particle_input_array, material, options, output_units); + } + } +} diff --git a/src/material.rs b/src/material.rs index 4941817..ae12ee9 100644 --- a/src/material.rs +++ b/src/material.rs @@ -1,395 +1,395 @@ -use super::*; - -/// Holds material input parameters from [material_params]. -#[derive(Deserialize, Clone)] -pub struct MaterialParameters { - pub energy_unit: String, - pub mass_unit: String, - pub Eb: Vec, - pub Es: Vec, - pub Ec: Vec, - pub Z: Vec, - pub m: Vec, - pub interaction_index: Vec, - pub surface_binding_model: SurfaceBindingModel, - pub bulk_binding_model: BulkBindingModel -} - -/// Material in rustbca. Includes the material properties and the mesh that defines the material geometry. -pub struct Material { - pub m: Vec, - pub Z: Vec, - pub Eb: Vec, - pub Es: Vec, - pub Ec: Vec, - pub interaction_index: Vec, - pub geometry: Box, - pub surface_binding_model: SurfaceBindingModel, - pub bulk_binding_model: BulkBindingModel -} -impl Material { - - pub fn new(material_parameters: &MaterialParameters, geometry_input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Material { - - let energy_unit: f64 = match material_parameters.energy_unit.as_str() { - "EV" => EV, - "J" => 1., - "KEV" => EV*1E3, - "MEV" => EV*1E6, - _ => material_parameters.energy_unit.parse() - .expect(format!( - "Input errror: could nor parse energy unit {}. Use a valid float or one of EV, J, KEV, MEV", &material_parameters.energy_unit.as_str() - ).as_str()), - }; - - let mass_unit: f64 = match material_parameters.mass_unit.as_str() { - "AMU" => AMU, - "KG" => 1., - _ => material_parameters.mass_unit.parse() - .expect(format!( - "Input errror: could nor parse mass unit {}. Use a valid float or one of AMU, KG", &material_parameters.mass_unit.as_str() - ).as_str()), - }; - - Material { - m: material_parameters.m.iter().map(|&i| i*mass_unit).collect(), - Z: material_parameters.Z.clone(), - Eb: material_parameters.Eb.iter().map(|&i| i*energy_unit).collect(), - Es: material_parameters.Es.iter().map(|&i| i*energy_unit).collect(), - Ec: material_parameters.Ec.iter().map(|&i| i*energy_unit).collect(), - interaction_index: material_parameters.interaction_index.clone(), - surface_binding_model: material_parameters.surface_binding_model, - bulk_binding_model: material_parameters.bulk_binding_model, - geometry: Box::new(T::new(geometry_input)), - } - } - - /// Gets concentrations of triangle that contains or is nearest to (x, y) - pub fn get_concentrations(&self, x: f64, y: f64, z: f64) -> Vec { - - let total_number_density: f64 = self.geometry.get_total_density(x, y, z); - - return self.geometry.get_densities(x, y, z).iter().map(|&i| i / total_number_density).collect(); - } - - /// Gets cumulative concentrations of triangle that contains or is nearest to (x, y). - /// Used for weighting the random choice of one of the constituent species. - pub fn get_cumulative_concentrations(&self, x: f64, y: f64, z: f64) -> Vec { - let mut sum: f64 = 0.; - let concentrations = self.geometry.get_concentrations(x, y, z); - let mut cumulative_concentrations = Vec::with_capacity(concentrations.len()); - for concentration in concentrations { - sum += concentration; - cumulative_concentrations.push(sum); - } - return cumulative_concentrations; - } - - /// Determines whether (x, y) is inside the material. - pub fn inside(&self, x: f64, y: f64, z: f64) -> bool { - return self.geometry.inside(x, y, z); - } - - /// Gets electronic stopping correction factor for LS and OR - pub fn electronic_stopping_correction_factor(&self, x: f64, y: f64, z: f64) -> f64 { - return self.geometry.get_ck(x, y, z); - } - - /// Determines the local mean free path from the formula sum(n(x, y))^(-1/3) - pub fn mfp(&self, x: f64, y: f64, z: f64) -> f64 { - return self.total_number_density(x, y, z).powf(-1./3.); - } - - /// Total number density of triangle that contains or is nearest to (x, y). - pub fn total_number_density(&self, x: f64, y: f64, z: f64) -> f64 { - self.geometry.get_densities(x, y, z).iter().sum::() - } - - /// Lists number density of each species of triangle that contains or is nearest to (x, y). - pub fn number_densities(&self, x: f64, y: f64, z: f64) -> &Vec { - return &self.geometry.get_densities(x, y, z); - } - - /// Determines whether a point (x, y) is inside the energy barrier of the material. - pub fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { - self.geometry.inside_energy_barrier(x, y, z) - } - - /// Determines whether a point (x, y) is inside the simulation boundary. - pub fn inside_simulation_boundary(&self, x:f64, y: f64, z: f64) -> bool { - return self.geometry.inside_simulation_boundary(x, y, z); - } - - /// Finds the closest point on the material boundary to the point (x, y). - pub fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { - self.geometry.closest_point(x, y, z) - } - - /// Finds the average, concentration-weighted atomic number, Z_effective, of the triangle that contains or is nearest to (x, y). - pub fn average_Z(&self, x: f64, y: f64, z: f64) -> f64 { - let concentrations = self.geometry.get_concentrations(x, y, z); - return self.Z.iter().zip(concentrations).map(|(charge, concentration)| charge*concentration).collect::>().iter().sum(); - } - - /// Finds the average, concentration-weighted atomic mass, m_effective, of the triangle that contains or is nearest to (x, y). - pub fn average_mass(&self, x: f64, y: f64, z: f64) -> f64 { - let concentrations = self.geometry.get_concentrations(x, y, z); - return self.m.iter().zip(concentrations).map(|(mass, concentration)| mass*concentration).collect::>().iter().sum(); - } - - /// Finds the average, concentration-weighted bulk binding energy of the triangle that contains or is nearest to (x, y). - pub fn average_bulk_binding_energy(&self, x: f64, y: f64, z: f64) -> f64 { - //returns average bulk binding energy - let concentrations = self.geometry.get_concentrations(x, y, z); - return self.Eb.iter().zip(concentrations).map(|(bulk_binding_energy, concentration)| bulk_binding_energy*concentration).collect::>().iter().sum(); - } - - pub fn actual_bulk_binding_energy(&self, species_index: usize, x: f64, y: f64, z: f64) -> f64 { - - match self.bulk_binding_model { - BulkBindingModel::INDIVIDUAL => self.Eb[species_index], - - BulkBindingModel::AVERAGE => { - if self.Eb[species_index] == 0. { - 0. - } else { - self.average_bulk_binding_energy(x, y, z) - } - } - } - } - - /// Finds the concentration-dependent surface binding energy of the triangle that contains or is nearest to (x, y). - /// The surface binding energy is calculated using one of three methods: - /// 1. INDIVIDUAL: the surface binding energies are set individually for each species, as Es. - /// 2. TARGET: the surface binding energy is calculated as the local concentration-weighted average of the target surface binding energies, unless the particle has Es = 0, in which case it is 0. - /// 3. AVERAGE: the surface binding energy is the average of the particle surface binding energy and the TARGET surface binding energy, unless either is 0 in which case it is 0. - pub fn actual_surface_binding_energy(&self, particle: &particle::Particle, x: f64, y: f64, z: f64) -> f64 { - let concentrations = self.geometry.get_concentrations(x, y, z); - - match self.surface_binding_model { - SurfaceBindingModel::TARGET => { - if particle.Es == 0. { - 0. - } else { - self.Es.iter().zip(concentrations).map(|(surface_binding_energy, concentration)| surface_binding_energy*concentration).collect::>().iter().sum() - } - }, - SurfaceBindingModel::INDIVIDUAL => particle.Es, - SurfaceBindingModel::AVERAGE => { - if (particle.Es == 0.) | (self.Es.iter().sum::() == 0.) { - 0. - } else { - 0.5*(particle.Es + self.Es.iter().zip(concentrations).map(|(surface_binding_energy, concentration)| surface_binding_energy*concentration).collect::>().iter().sum::()) - } - }, - SurfaceBindingModel::ISOTROPIC{calculation} | SurfaceBindingModel::PLANAR{calculation} => { - match calculation { - SurfaceBindingCalculation::INDIVIDUAL => particle.Es, - - SurfaceBindingCalculation::TARGET => { - if particle.Es == 0. { - 0. - } else { - self.Es.iter().zip(concentrations).map(|(surface_binding_energy, concentration)| surface_binding_energy*concentration).collect::>().iter().sum() - } - }, - - SurfaceBindingCalculation::AVERAGE => { - if (particle.Es == 0.) | (self.Es.iter().sum::() == 0.) { - 0. - } else { - 0.5*(particle.Es + self.Es.iter().zip(concentrations).map(|(surface_binding_energy, concentration)| surface_binding_energy*concentration).collect::>().iter().sum::()) - } - }, - } - } - } - } - - ///The minimum cutoff energy of all species that make up the material. - pub fn minimum_cutoff_energy(&self) -> f64 { - let mut min_Ec = self.Ec.iter().sum::(); - for Ec in self.Ec.iter() { - if min_Ec > *Ec { - min_Ec = *Ec; - } - } - return min_Ec; - } - - ///Choose the parameters of a target atom as a concentration-weighted random draw from the species in the triangle that contains or is nearest to (x, y). - pub fn choose(&self, x: f64, y: f64, z: f64) -> (usize, f64, f64, f64, f64, usize) { - let random_number: f64 = rand::random::(); - let cumulative_concentrations = self.get_cumulative_concentrations(x, y, z); - - for (component_index, cumulative_concentration) in cumulative_concentrations.iter().enumerate() { - if random_number < *cumulative_concentration { - return (component_index, self.Z[component_index], self.m[component_index], self.Ec[component_index], self.Es[component_index], self.interaction_index[component_index]); - } - } - panic!("Input error: method choose() operation failed to choose a valid species. Check densities."); - } - - /// Calculate the electronic stopping cross-sections using the mode set in [options]. - pub fn electronic_stopping_cross_sections(&self, particle_1: &super::particle::Particle, electronic_stopping_mode: ElectronicStoppingMode) -> Vec { - - let E = particle_1.E; - let Ma = particle_1.m; - let Za = particle_1.Z; - - let mut stopping_powers = Vec::with_capacity(self.Z.len()); - - //Bragg's rule: total stopping power is sum of stopping powers of individual atoms - //Therefore, compute each stopping power separately, and add them up - - let x = particle_1.pos.x; - let y = particle_1.pos.y; - let z = particle_1.pos.y; - let ck = self.electronic_stopping_correction_factor(x, y, z); - - for (n, Zb) in self.number_densities(x, y, z).iter().zip(&self.Z) { - - let beta = (1. - (1. + E/Ma/C.powi(2)).powf(-2.)).sqrt(); - let v = beta*C; - - // This term is an empirical fit to the mean ionization potential - let I0 = match *Zb < 13. { - true => 12. + 7./Zb, - false => 9.76 + 58.5*Zb.powf(-1.19), - }; - let I = Zb*I0*Q; - - //See Biersack and Haggmark - this looks like an empirical shell correction - let B = match *Zb < 3. { - true => 100.*Za/Zb, - false => 5. - }; - - //Bethe stopping modified by Biersack and Varelas - let prefactor = BETHE_BLOCH_PREFACTOR*Zb*Za*Za/beta/beta; - let eb = 2.*ME*v*v/I; - let S_high = prefactor*(eb + 1. + B/eb).ln(); - - //Lindhard-Scharff electronic stopping - let S_low = LINDHARD_SCHARFF_PREFACTOR*(Za.powf(7./6.)*Zb)/(Za.powf(2./3.) + Zb.powf(2./3.)).powf(3./2.)*(E/Q/Ma*AMU).sqrt(); - - let stopping_power = match electronic_stopping_mode { - //Biersack-Varelas Interpolation - ElectronicStoppingMode::INTERPOLATED => 1./(1./S_high + 1./S_low*ck), - //Oen-Robinson - ElectronicStoppingMode::LOW_ENERGY_LOCAL => S_low*ck, - //Lindhard-Scharff - ElectronicStoppingMode::LOW_ENERGY_NONLOCAL => S_low*ck, - //Lindhard-Scharff and Oen-Robinson, using Lindhard Equipartition - ElectronicStoppingMode::LOW_ENERGY_EQUIPARTITION => S_low*ck, - }; - - stopping_powers.push(stopping_power); - } - return stopping_powers; - } -} - -/// Calculate the effects of the planar surface binding potential of a material on a particle. -/// These effects include surface reflection and refraction of particles with non-zero surface binding energies. -pub fn surface_binding_energy(particle_1: &mut particle::Particle, material: &material::Material) { - let x = particle_1.pos.x; - let y = particle_1.pos.y; - let z = particle_1.pos.z; - let x_old = particle_1.pos_old.x; - let y_old = particle_1.pos_old.y; - let z_old = particle_1.pos_old.z; - let cosx = particle_1.dir.x; - let cosy = particle_1.dir.y; - let cosz = particle_1.dir.z; - let E = particle_1.E; - - //Actual surface binding energies - let Es = material.actual_surface_binding_energy(particle_1, x_old, y_old, z_old); - //println!("Actual Es: {}", Es); - let Ec = particle_1.Ec; - - let inside_now = material.inside_energy_barrier(x, y, z); - let inside_old = material.inside_energy_barrier(x_old, y_old, z_old); - - let leaving = !inside_now & inside_old; - let entering = inside_now & !inside_old; - - if entering { - if particle_1.backreflected { - particle_1.backreflected = false; - } else { - let (x2, y2, z2) = material.closest_point(x, y, z); - let dx = x2 - x; - let dy = y2 - y; - let dz = z2 - z; - let mag = (dx*dx + dy*dy + dz*dz).sqrt(); - let costheta = dx*cosx/mag + dy*cosy/mag + dz*cosz/mag; - - match material.surface_binding_model { - SurfaceBindingModel::ISOTROPIC{..} => particle_1.E += Es, - _ => particle::surface_refraction(particle_1, Vector::new(dx/mag, dy/mag, dz/mag), Es) - } - } - } - - if leaving { - - let (x2, y2, z2) = material.closest_point(x, y, z); - let dx = x2 - x; - let dy = y2 - y; - let dz = z2 - z; - let mag = (dx*dx + dy*dy + dz*dz).sqrt(); - let costheta = dx*cosx/mag + dy*cosy/mag + dz*cosz/mag; - - let leaving_energy = match material.surface_binding_model { - SurfaceBindingModel::ISOTROPIC{..} => E, - _ => E*costheta*costheta, - }; - - if costheta < 0. { - if leaving_energy > Es { - - match material.surface_binding_model { - SurfaceBindingModel::PLANAR{..} => { - particle::surface_refraction(particle_1, Vector::new(-dx/mag, -dy/mag, -dz/mag), -Es); - } - _ => particle_1.E += -Es, - } - - } else { - - //Specular reflection at local surface normal - particle_1.dir.x = -2.*(costheta)*dx/mag + cosx; - particle_1.dir.y = -2.*(costheta)*dy/mag + cosy; - particle_1.dir.z = -2.*(costheta)*dz/mag + cosz; - - particle_1.backreflected = true; - } - } - } -} - -/// Apply the boundary conditions of a material on a particle, including stopping, leaving, and reflection/refraction by/through the surface binding potential. -pub fn boundary_condition_planar(particle_1: &mut particle::Particle, material: &material::Material) { - let x = particle_1.pos.x; - let y = particle_1.pos.y; - let z = particle_1.pos.z; - let E = particle_1.E; - let Ec = particle_1.Ec; - - if !material.inside_simulation_boundary(x, y, z) { - particle_1.left = true; - particle_1.add_trajectory(); - } - - if (E < Ec) & !particle_1.left & material.inside_energy_barrier(x, y, z) { - particle_1.stopped = true; - particle_1.add_trajectory(); - } - - if !particle_1.stopped & !particle_1.left { - surface_binding_energy(particle_1, material); - } -} +use super::*; + +/// Holds material input parameters from [material_params]. +#[derive(Deserialize, Clone)] +pub struct MaterialParameters { + pub energy_unit: String, + pub mass_unit: String, + pub Eb: Vec, + pub Es: Vec, + pub Ec: Vec, + pub Z: Vec, + pub m: Vec, + pub interaction_index: Vec, + pub surface_binding_model: SurfaceBindingModel, + pub bulk_binding_model: BulkBindingModel +} + +/// Material in rustbca. Includes the material properties and the mesh that defines the material geometry. +pub struct Material { + pub m: Vec, + pub Z: Vec, + pub Eb: Vec, + pub Es: Vec, + pub Ec: Vec, + pub interaction_index: Vec, + pub geometry: Box, + pub surface_binding_model: SurfaceBindingModel, + pub bulk_binding_model: BulkBindingModel +} +impl Material { + + pub fn new(material_parameters: &MaterialParameters, geometry_input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Material { + + let energy_unit: f64 = match material_parameters.energy_unit.as_str() { + "EV" => EV, + "J" => 1., + "KEV" => EV*1E3, + "MEV" => EV*1E6, + _ => material_parameters.energy_unit.parse() + .expect(format!( + "Input errror: could nor parse energy unit {}. Use a valid float or one of EV, J, KEV, MEV", &material_parameters.energy_unit.as_str() + ).as_str()), + }; + + let mass_unit: f64 = match material_parameters.mass_unit.as_str() { + "AMU" => AMU, + "KG" => 1., + _ => material_parameters.mass_unit.parse() + .expect(format!( + "Input errror: could nor parse mass unit {}. Use a valid float or one of AMU, KG", &material_parameters.mass_unit.as_str() + ).as_str()), + }; + + Material { + m: material_parameters.m.iter().map(|&i| i*mass_unit).collect(), + Z: material_parameters.Z.clone(), + Eb: material_parameters.Eb.iter().map(|&i| i*energy_unit).collect(), + Es: material_parameters.Es.iter().map(|&i| i*energy_unit).collect(), + Ec: material_parameters.Ec.iter().map(|&i| i*energy_unit).collect(), + interaction_index: material_parameters.interaction_index.clone(), + surface_binding_model: material_parameters.surface_binding_model, + bulk_binding_model: material_parameters.bulk_binding_model, + geometry: Box::new(T::new(geometry_input)), + } + } + + /// Gets concentrations of triangle that contains or is nearest to (x, y) + pub fn get_concentrations(&self, x: f64, y: f64, z: f64) -> Vec { + + let total_number_density: f64 = self.geometry.get_total_density(x, y, z); + + return self.geometry.get_densities(x, y, z).iter().map(|&i| i / total_number_density).collect(); + } + + /// Gets cumulative concentrations of triangle that contains or is nearest to (x, y). + /// Used for weighting the random choice of one of the constituent species. + pub fn get_cumulative_concentrations(&self, x: f64, y: f64, z: f64) -> Vec { + let mut sum: f64 = 0.; + let concentrations = self.geometry.get_concentrations(x, y, z); + let mut cumulative_concentrations = Vec::with_capacity(concentrations.len()); + for concentration in concentrations { + sum += concentration; + cumulative_concentrations.push(sum); + } + return cumulative_concentrations; + } + + /// Determines whether (x, y) is inside the material. + pub fn inside(&self, x: f64, y: f64, z: f64) -> bool { + return self.geometry.inside(x, y, z); + } + + /// Gets electronic stopping correction factor for LS and OR + pub fn electronic_stopping_correction_factor(&self, x: f64, y: f64, z: f64) -> f64 { + return self.geometry.get_ck(x, y, z); + } + + /// Determines the local mean free path from the formula sum(n(x, y))^(-1/3) + pub fn mfp(&self, x: f64, y: f64, z: f64) -> f64 { + return self.total_number_density(x, y, z).powf(-1./3.); + } + + /// Total number density of triangle that contains or is nearest to (x, y). + pub fn total_number_density(&self, x: f64, y: f64, z: f64) -> f64 { + self.geometry.get_densities(x, y, z).iter().sum::() + } + + /// Lists number density of each species of triangle that contains or is nearest to (x, y). + pub fn number_densities(&self, x: f64, y: f64, z: f64) -> &Vec { + return &self.geometry.get_densities(x, y, z); + } + + /// Determines whether a point (x, y) is inside the energy barrier of the material. + pub fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { + self.geometry.inside_energy_barrier(x, y, z) + } + + /// Determines whether a point (x, y) is inside the simulation boundary. + pub fn inside_simulation_boundary(&self, x:f64, y: f64, z: f64) -> bool { + return self.geometry.inside_simulation_boundary(x, y, z); + } + + /// Finds the closest point on the material boundary to the point (x, y). + pub fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { + self.geometry.closest_point(x, y, z) + } + + /// Finds the average, concentration-weighted atomic number, Z_effective, of the triangle that contains or is nearest to (x, y). + pub fn average_Z(&self, x: f64, y: f64, z: f64) -> f64 { + let concentrations = self.geometry.get_concentrations(x, y, z); + return self.Z.iter().zip(concentrations).map(|(charge, concentration)| charge*concentration).collect::>().iter().sum(); + } + + /// Finds the average, concentration-weighted atomic mass, m_effective, of the triangle that contains or is nearest to (x, y). + pub fn average_mass(&self, x: f64, y: f64, z: f64) -> f64 { + let concentrations = self.geometry.get_concentrations(x, y, z); + return self.m.iter().zip(concentrations).map(|(mass, concentration)| mass*concentration).collect::>().iter().sum(); + } + + /// Finds the average, concentration-weighted bulk binding energy of the triangle that contains or is nearest to (x, y). + pub fn average_bulk_binding_energy(&self, x: f64, y: f64, z: f64) -> f64 { + //returns average bulk binding energy + let concentrations = self.geometry.get_concentrations(x, y, z); + return self.Eb.iter().zip(concentrations).map(|(bulk_binding_energy, concentration)| bulk_binding_energy*concentration).collect::>().iter().sum(); + } + + pub fn actual_bulk_binding_energy(&self, species_index: usize, x: f64, y: f64, z: f64) -> f64 { + + match self.bulk_binding_model { + BulkBindingModel::INDIVIDUAL => self.Eb[species_index], + + BulkBindingModel::AVERAGE => { + if self.Eb[species_index] == 0. { + 0. + } else { + self.average_bulk_binding_energy(x, y, z) + } + } + } + } + + /// Finds the concentration-dependent surface binding energy of the triangle that contains or is nearest to (x, y). + /// The surface binding energy is calculated using one of three methods: + /// 1. INDIVIDUAL: the surface binding energies are set individually for each species, as Es. + /// 2. TARGET: the surface binding energy is calculated as the local concentration-weighted average of the target surface binding energies, unless the particle has Es = 0, in which case it is 0. + /// 3. AVERAGE: the surface binding energy is the average of the particle surface binding energy and the TARGET surface binding energy, unless either is 0 in which case it is 0. + pub fn actual_surface_binding_energy(&self, particle: &particle::Particle, x: f64, y: f64, z: f64) -> f64 { + let concentrations = self.geometry.get_concentrations(x, y, z); + + match self.surface_binding_model { + SurfaceBindingModel::TARGET => { + if particle.Es == 0. { + 0. + } else { + self.Es.iter().zip(concentrations).map(|(surface_binding_energy, concentration)| surface_binding_energy*concentration).collect::>().iter().sum() + } + }, + SurfaceBindingModel::INDIVIDUAL => particle.Es, + SurfaceBindingModel::AVERAGE => { + if (particle.Es == 0.) | (self.Es.iter().sum::() == 0.) { + 0. + } else { + 0.5*(particle.Es + self.Es.iter().zip(concentrations).map(|(surface_binding_energy, concentration)| surface_binding_energy*concentration).collect::>().iter().sum::()) + } + }, + SurfaceBindingModel::ISOTROPIC{calculation} | SurfaceBindingModel::PLANAR{calculation} => { + match calculation { + SurfaceBindingCalculation::INDIVIDUAL => particle.Es, + + SurfaceBindingCalculation::TARGET => { + if particle.Es == 0. { + 0. + } else { + self.Es.iter().zip(concentrations).map(|(surface_binding_energy, concentration)| surface_binding_energy*concentration).collect::>().iter().sum() + } + }, + + SurfaceBindingCalculation::AVERAGE => { + if (particle.Es == 0.) | (self.Es.iter().sum::() == 0.) { + 0. + } else { + 0.5*(particle.Es + self.Es.iter().zip(concentrations).map(|(surface_binding_energy, concentration)| surface_binding_energy*concentration).collect::>().iter().sum::()) + } + }, + } + } + } + } + + ///The minimum cutoff energy of all species that make up the material. + pub fn minimum_cutoff_energy(&self) -> f64 { + let mut min_Ec = self.Ec.iter().sum::(); + for Ec in self.Ec.iter() { + if min_Ec > *Ec { + min_Ec = *Ec; + } + } + return min_Ec; + } + + ///Choose the parameters of a target atom as a concentration-weighted random draw from the species in the triangle that contains or is nearest to (x, y). + pub fn choose(&self, x: f64, y: f64, z: f64) -> (usize, f64, f64, f64, f64, usize) { + let random_number: f64 = rand::random::(); + let cumulative_concentrations = self.get_cumulative_concentrations(x, y, z); + + for (component_index, cumulative_concentration) in cumulative_concentrations.iter().enumerate() { + if random_number < *cumulative_concentration { + return (component_index, self.Z[component_index], self.m[component_index], self.Ec[component_index], self.Es[component_index], self.interaction_index[component_index]); + } + } + panic!("Input error: method choose() operation failed to choose a valid species. Check densities."); + } + + /// Calculate the electronic stopping cross-sections using the mode set in [options]. + pub fn electronic_stopping_cross_sections(&self, particle_1: &super::particle::Particle, electronic_stopping_mode: ElectronicStoppingMode) -> Vec { + + let E = particle_1.E; + let Ma = particle_1.m; + let Za = particle_1.Z; + + let mut stopping_powers = Vec::with_capacity(self.Z.len()); + + //Bragg's rule: total stopping power is sum of stopping powers of individual atoms + //Therefore, compute each stopping power separately, and add them up + + let x = particle_1.pos.x; + let y = particle_1.pos.y; + let z = particle_1.pos.y; + let ck = self.electronic_stopping_correction_factor(x, y, z); + + for (n, Zb) in self.number_densities(x, y, z).iter().zip(&self.Z) { + + let beta = (1. - (1. + E/Ma/C.powi(2)).powf(-2.)).sqrt(); + let v = beta*C; + + // This term is an empirical fit to the mean ionization potential + let I0 = match *Zb < 13. { + true => 12. + 7./Zb, + false => 9.76 + 58.5*Zb.powf(-1.19), + }; + let I = Zb*I0*Q; + + //See Biersack and Haggmark - this looks like an empirical shell correction + let B = match *Zb < 3. { + true => 100.*Za/Zb, + false => 5. + }; + + //Bethe stopping modified by Biersack and Varelas + let prefactor = BETHE_BLOCH_PREFACTOR*Zb*Za*Za/beta/beta; + let eb = 2.*ME*v*v/I; + let S_high = prefactor*(eb + 1. + B/eb).ln(); + + //Lindhard-Scharff electronic stopping + let S_low = LINDHARD_SCHARFF_PREFACTOR*(Za.powf(7./6.)*Zb)/(Za.powf(2./3.) + Zb.powf(2./3.)).powf(3./2.)*(E/Q/Ma*AMU).sqrt(); + + let stopping_power = match electronic_stopping_mode { + //Biersack-Varelas Interpolation + ElectronicStoppingMode::INTERPOLATED => 1./(1./S_high + 1./S_low*ck), + //Oen-Robinson + ElectronicStoppingMode::LOW_ENERGY_LOCAL => S_low*ck, + //Lindhard-Scharff + ElectronicStoppingMode::LOW_ENERGY_NONLOCAL => S_low*ck, + //Lindhard-Scharff and Oen-Robinson, using Lindhard Equipartition + ElectronicStoppingMode::LOW_ENERGY_EQUIPARTITION => S_low*ck, + }; + + stopping_powers.push(stopping_power); + } + return stopping_powers; + } +} + +/// Calculate the effects of the planar surface binding potential of a material on a particle. +/// These effects include surface reflection and refraction of particles with non-zero surface binding energies. +pub fn surface_binding_energy(particle_1: &mut particle::Particle, material: &material::Material) { + let x = particle_1.pos.x; + let y = particle_1.pos.y; + let z = particle_1.pos.z; + let x_old = particle_1.pos_old.x; + let y_old = particle_1.pos_old.y; + let z_old = particle_1.pos_old.z; + let cosx = particle_1.dir.x; + let cosy = particle_1.dir.y; + let cosz = particle_1.dir.z; + let E = particle_1.E; + + //Actual surface binding energies + let Es = material.actual_surface_binding_energy(particle_1, x_old, y_old, z_old); + //println!("Actual Es: {}", Es); + let Ec = particle_1.Ec; + + let inside_now = material.inside_energy_barrier(x, y, z); + let inside_old = material.inside_energy_barrier(x_old, y_old, z_old); + + let leaving = !inside_now & inside_old; + let entering = inside_now & !inside_old; + + if entering { + if particle_1.backreflected { + particle_1.backreflected = false; + } else { + let (x2, y2, z2) = material.closest_point(x, y, z); + let dx = x2 - x; + let dy = y2 - y; + let dz = z2 - z; + let mag = (dx*dx + dy*dy + dz*dz).sqrt(); + let costheta = dx*cosx/mag + dy*cosy/mag + dz*cosz/mag; + + match material.surface_binding_model { + SurfaceBindingModel::ISOTROPIC{..} => particle_1.E += Es, + _ => particle::surface_refraction(particle_1, Vector::new(dx/mag, dy/mag, dz/mag), Es) + } + } + } + + if leaving { + + let (x2, y2, z2) = material.closest_point(x, y, z); + let dx = x2 - x; + let dy = y2 - y; + let dz = z2 - z; + let mag = (dx*dx + dy*dy + dz*dz).sqrt(); + let costheta = dx*cosx/mag + dy*cosy/mag + dz*cosz/mag; + + let leaving_energy = match material.surface_binding_model { + SurfaceBindingModel::ISOTROPIC{..} => E, + _ => E*costheta*costheta, + }; + + if costheta < 0. { + if leaving_energy > Es { + + match material.surface_binding_model { + SurfaceBindingModel::PLANAR{..} => { + particle::surface_refraction(particle_1, Vector::new(-dx/mag, -dy/mag, -dz/mag), -Es); + } + _ => particle_1.E += -Es, + } + + } else { + + //Specular reflection at local surface normal + particle_1.dir.x = -2.*(costheta)*dx/mag + cosx; + particle_1.dir.y = -2.*(costheta)*dy/mag + cosy; + particle_1.dir.z = -2.*(costheta)*dz/mag + cosz; + + particle_1.backreflected = true; + } + } + } +} + +/// Apply the boundary conditions of a material on a particle, including stopping, leaving, and reflection/refraction by/through the surface binding potential. +pub fn boundary_condition_planar(particle_1: &mut particle::Particle, material: &material::Material) { + let x = particle_1.pos.x; + let y = particle_1.pos.y; + let z = particle_1.pos.z; + let E = particle_1.E; + let Ec = particle_1.Ec; + + if !material.inside_simulation_boundary(x, y, z) { + particle_1.left = true; + particle_1.add_trajectory(); + } + + if (E < Ec) & !particle_1.left & material.inside_energy_barrier(x, y, z) { + particle_1.stopped = true; + particle_1.add_trajectory(); + } + + if !particle_1.stopped & !particle_1.left { + surface_binding_energy(particle_1, material); + } +} diff --git a/src/output.rs b/src/output.rs index 41c6971..da0c27c 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,419 +1,419 @@ -use super::*; -use std::fs::File; - -pub struct OutputUnits { - pub length_unit: f64, - pub energy_unit: f64, - pub mass_unit: f64 -} - -/// Converts from 6D particle coordinates to energy, angle coordinates w.r.t. negative x-axis -pub fn energy_angle_from_particle(particle: &particle::Particle, units: &OutputUnits) -> (f64, f64) { - let energy = particle.E/units.energy_unit; - let ux = particle.dir.x; - let uy = particle.dir.y; - let uz = particle.dir.z; - - let vyz = ((uy).powi(2) + (uz).powi(2)).sqrt(); - let angle = vyz.atan2(-ux) * 180.0 / PI; - - (energy, angle) -} - -#[cfg(feature = "distributions")] -extern crate ndarray; - -#[cfg(feature = "distributions")] -use ndarray::prelude::*; - -/// Distribution tracker for tracking EADs and implantation distributions -#[derive(Serialize)] -#[cfg(feature = "distributions")] -pub struct Distributions { - pub energies: Array1, - pub angles: Array1, - pub x_range: Array1, - pub y_range: Array1, - pub z_range: Array1, - pub reflected_ead: Array2, - pub sputtered_ead: Array2, - pub implanted_x: Array1, - pub implanted_y: Array1, - pub implanted_z: Array1, - pub electronic_energy_loss_x: Array1, - pub nuclear_energy_loss_x: Array1, -} - -#[cfg(feature = "distributions")] -impl Distributions { - pub fn new(options: &Options) -> Distributions { - Distributions { - energies: Array::linspace(options.energy_min, options.energy_max, options.energy_num), - angles: Array::linspace(options.angle_min, options.angle_max, options.angle_num), - x_range: Array::linspace(options.x_min, options.x_max, options.x_num), - y_range: Array::linspace(options.y_min, options.y_max, options.y_num), - z_range: Array::linspace(options.z_min, options.z_max, options.z_num), - reflected_ead: Array::zeros((options.energy_num, options.angle_num)), - sputtered_ead: Array::zeros((options.energy_num, options.angle_num)), - implanted_x: Array::zeros(options.x_num), - implanted_y: Array::zeros(options.y_num), - implanted_z: Array::zeros(options.z_num), - electronic_energy_loss_x: Array::zeros(options.x_num), - nuclear_energy_loss_x: Array::zeros(options.x_num), - } - } - - /// Write distributions to toml - pub fn print(&self, options: &Options) { - - let distribution_output_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "distributions.toml")) - .context("Could not open distributions output file.") - .unwrap(); - let mut distribution_file_stream = BufWriter::with_capacity(8000, distribution_output_file); - let toml = toml::to_string(&self).unwrap(); - writeln!(distribution_file_stream, "{}", toml).unwrap(); - - } - - /// Updates distributions with a single particle - pub fn update(&mut self, particle: &particle::Particle, units: &OutputUnits, options: &Options, num_incident: usize) { - let (energy, angle) = energy_angle_from_particle(particle, units); - - let delta_energy = self.energies[1] - self.energies[0]; - let energy_index_left: i32 = ((energy - self.energies[0])/delta_energy).floor() as i32; - - let delta_angle = self.angles[1] - self.angles[0]; - let angle_index_left: i32 = ((angle - self.angles[0])/delta_angle).floor() as i32; - - let inside_energy = (energy_index_left >= 0) & (energy_index_left < self.energies.len() as i32); - let inside_angle = (angle_index_left >= 0) & (angle_index_left < self.angles.len() as i32); - - if particle.incident & particle.left { - if inside_energy & inside_angle { - self.reflected_ead[[energy_index_left as usize, angle_index_left as usize]] += 1; - } - } - - if !particle.incident & particle.left { - if inside_energy & inside_angle { - self.sputtered_ead[[energy_index_left as usize, angle_index_left as usize]] += 1; - } - } - - if particle.incident & particle.stopped { - let x = particle.pos.x/units.length_unit; - let y = particle.pos.y/units.length_unit; - let z = particle.pos.z/units.length_unit; - - let delta_x = self.x_range[1] - self.x_range[0]; - let delta_y = self.y_range[1] - self.y_range[0]; - let delta_z = self.z_range[1] - self.z_range[0]; - - let x_index_left: i32 = ((x - self.x_range[0])/delta_x) as i32; - let y_index_left: i32 = ((y - self.y_range[0])/delta_y) as i32; - let z_index_left: i32 = ((z - self.z_range[0])/delta_z) as i32; - - let inside_x = (x_index_left >= 0) & (x_index_left < self.x_range.len() as i32); - let inside_y = (y_index_left >= 0) & (y_index_left < self.y_range.len() as i32); - let inside_z = (z_index_left >= 0) & (z_index_left < self.z_range.len() as i32); - - if inside_x { - self.implanted_x[x_index_left as usize] += 1; - } - if inside_y { - self.implanted_y[y_index_left as usize] += 1; - } - if inside_z { - self.implanted_z[z_index_left as usize] += 1; - } - } - - if options.track_energy_losses & particle.incident { - - for energy_loss in particle.energies.iter() { - let x = energy_loss.x/units.length_unit; - let delta_x = self.x_range[1] - self.x_range[0]; - - let electronic_energy_loss = energy_loss.Ee/units.energy_unit/(num_incident as f64)/delta_x; - let nuclear_energy_loss = energy_loss.En/units.energy_unit/(num_incident as f64)/delta_x; - - let x_index_left: i32 = ((x - self.x_range[0])/delta_x) as i32; - let inside_x = (x_index_left >= 0) & (x_index_left < self.x_range.len() as i32); - - if inside_x { - self.electronic_energy_loss_x[x_index_left as usize] += electronic_energy_loss; - self.nuclear_energy_loss_x[x_index_left as usize] += nuclear_energy_loss; - } - } - } - } -} - -/// File streams for list output -pub struct OutputListStreams { - reflected_file_stream: BufWriter, - sputtered_file_stream: BufWriter, - deposited_file_stream: BufWriter, - trajectory_file_stream: BufWriter, - trajectory_data_stream: BufWriter, - displacements_file_stream: BufWriter, - energy_loss_file_stream: BufWriter, -} - -/// Simulation-wide summary output tracker. -pub struct Summary { - pub num_incident: u64, - pub num_sputtered: u64, - pub num_reflected: u64, -} - -/// Summary tracker of sputtering and reflection -impl Summary { - pub fn new(num_incident: u64) -> Summary { - Summary { - num_incident, - num_sputtered: 0, - num_reflected: 0, - } - } - - pub fn add(&mut self, particle: &particle::Particle) { - if particle.incident & particle.left { - self.num_reflected += 1; - } - if !particle.incident & particle.left { - self.num_sputtered += 1; - } - } -} - -pub struct SummaryPerSpecies { - pub m: Vec, - pub sputtered: Vec, - pub reflected: Vec, - pub deposited: Vec, - pub summary_stream_file: BufWriter, -} - -impl SummaryPerSpecies { - pub fn new(options: &Options) -> SummaryPerSpecies { - - let summary_output_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "summary.output")) - .context("Could not open output file.") - .unwrap(); - let writer = BufWriter::with_capacity(8000, summary_output_file); - - SummaryPerSpecies { - m: vec![], - sputtered: vec![], - reflected: vec![], - deposited: vec![], - summary_stream_file: writer, - } - } - - pub fn print(&mut self, options: &Options, output_units: &OutputUnits) { - //Write to summary file - writeln!(self.summary_stream_file, "mass, reflected, sputtered, deposited") - .expect(format!("Output error: could not write to {}summary.output.", options.name).as_str()); - - for (mass, reflected, sputtered, deposited) in izip!(&self.m, &self.reflected, &self.sputtered, &self.deposited) { - writeln!(self.summary_stream_file, "{}, {}, {}, {},", mass/output_units.mass_unit, reflected, sputtered, deposited) - .expect(format!("Output error: could not write to {}summary.output.", options.name).as_str()); - } - self.summary_stream_file.flush().unwrap(); - } - - pub fn update(&mut self, particle: &particle::Particle) { - - if self.m.contains(&(particle.m)) { - - let index = self.m.iter().position(|m| *m == particle.m).unwrap(); - - match (particle.incident, particle.left) { - (true, true) => self.reflected[index] += 1, - (true, false) => self.deposited[index] += 1, - (false, true) => self.sputtered[index] += 1, - _ => (), - } - } else { - self.m.push(particle.m); - match (particle.incident, particle.left) { - (true, true) => {self.reflected.push(1); self.deposited.push(0); self.sputtered.push(0);}, - (true, false) => {self.reflected.push(0); self.deposited.push(1); self.sputtered.push(0);}, - (false, true) => {self.reflected.push(0); self.deposited.push(0); self.sputtered.push(1);}, - _ => {self.reflected.push(0); self.deposited.push(0); self.sputtered.push(0);}, - } - } - } -} - -/// Open list output files for streaming write -pub fn open_output_lists(options: &Options) -> OutputListStreams { - //Open output files for streaming output - let reflected_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "reflected.output")) - .context("Could not open output file.") - .unwrap(); - let reflected_file_stream = BufWriter::with_capacity(options.write_buffer_size, reflected_file); - - let sputtered_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "sputtered.output")) - .context("Could not open output file.") - .unwrap(); - let sputtered_file_stream = BufWriter::with_capacity(options.write_buffer_size, sputtered_file); - - let deposited_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "deposited.output")) - .context("Could not open output file.") - .unwrap(); - let deposited_file_stream = BufWriter::with_capacity(options.write_buffer_size, deposited_file); - - let trajectory_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "trajectories.output")) - .context("Could not open output file.") - .unwrap(); - let trajectory_file_stream = BufWriter::with_capacity(options.write_buffer_size, trajectory_file); - - let trajectory_data = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "trajectory_data.output")) - .context("Could not open output file.") - .unwrap(); - let trajectory_data_stream = BufWriter::with_capacity(options.write_buffer_size, trajectory_data); - - let displacements_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "displacements.output")) - .context("Could not open output file.") - .unwrap(); - let displacements_file_stream = BufWriter::with_capacity(options.write_buffer_size, displacements_file); - - let energy_loss_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(format!("{}{}", options.name, "energy_loss.output")) - .context("Could not open output file.") - .unwrap(); - let energy_loss_file_stream = BufWriter::with_capacity(options.write_buffer_size, energy_loss_file); - - OutputListStreams { - reflected_file_stream, - sputtered_file_stream, - deposited_file_stream, - trajectory_file_stream, - trajectory_data_stream, - displacements_file_stream, - energy_loss_file_stream, - } -} - -/// Write output lists -pub fn output_lists(output_list_streams: &mut OutputListStreams, particle: particle::Particle, options: &Options, output_units: &OutputUnits) { - - let length_unit = output_units.length_unit; - let energy_unit = output_units.energy_unit; - let mass_unit = output_units.mass_unit; - - if !particle.incident & options.track_displacements { - writeln!( - output_list_streams.displacements_file_stream, "{},{},{},{},{},{},{},{},{}", - particle.m/mass_unit, particle.Z, particle.energy_origin/energy_unit, - particle.pos_origin.x/length_unit, particle.pos_origin.y/length_unit, particle.pos_origin.z/length_unit, - particle.pos.x/length_unit, particle.pos.y/length_unit, particle.pos.z/length_unit - ).expect(format!("Output error: could not write to {}displacements.output.", options.name).as_str()); - } - - //Incident particle, left simulation: reflected - if particle.incident & particle.left { - writeln!( - output_list_streams.reflected_file_stream, "{},{},{},{},{},{},{},{},{},{}", - particle.m/mass_unit, particle.Z, particle.E/energy_unit, - particle.pos.x/length_unit, particle.pos.y/length_unit, particle.pos.z/length_unit, - particle.dir.x, particle.dir.y, particle.dir.z, - particle.number_collision_events - ).expect(format!("Output error: could not write to {}reflected.output.", options.name).as_str()); - } - - //Incident particle, stopped in material: deposited - if particle.incident & particle.stopped { - writeln!( - output_list_streams.deposited_file_stream, "{},{},{},{},{},{}", - particle.m/mass_unit, particle.Z, - particle.pos.x/length_unit, particle.pos.y/length_unit, particle.pos.z/length_unit, - particle.number_collision_events - ).expect(format!("Output error: could not write to {}deposited.output.", options.name).as_str()); - } - - //Not an incident particle, left material: sputtered - if !particle.incident & particle.left { - writeln!( - output_list_streams.sputtered_file_stream, "{},{},{},{},{},{},{},{},{},{},{},{},{}", - particle.m/mass_unit, particle.Z, particle.E/energy_unit, - particle.pos.x/length_unit, particle.pos.y/length_unit, particle.pos.z/length_unit, - particle.dir.x, particle.dir.y, particle.dir.z, - particle.number_collision_events, - particle.pos_origin.x/length_unit, particle.pos_origin.y/length_unit, particle.pos_origin.z/length_unit - ).expect(format!("Output error: could not write to {}sputtered.output.", options.name).as_str()); - } - - //Trajectory output - if particle.track_trajectories { - writeln!(output_list_streams.trajectory_data_stream, "{}", particle.trajectory.len()) - .expect(format!("Output error: could not write to {}trajectory_data.output.", options.name).as_str()); - - for pos in particle.trajectory { - writeln!( - output_list_streams.trajectory_file_stream, "{},{},{},{},{},{}", - particle.m/mass_unit, particle.Z, pos.E/energy_unit, - pos.x/length_unit, pos.y/length_unit, pos.z/length_unit, - ).expect(format!("Output error: could not write to {}trajectories.output.", options.name).as_str()); - } - } - - if particle.incident & options.track_energy_losses { - for energy_loss in particle.energies { - writeln!( - output_list_streams.energy_loss_file_stream, "{},{},{},{},{},{},{}", - particle.m/mass_unit, particle.Z, - energy_loss.En/energy_unit, energy_loss.Ee/energy_unit, - energy_loss.x/length_unit, energy_loss.y/length_unit, energy_loss.z/length_unit, - ).expect(format!("Output error: could not write to {}energy_loss.output.", options.name).as_str()); - } - } -} - -/// Flush output list streams -pub fn output_list_flush(output_list_streams: &mut OutputListStreams) { - output_list_streams.displacements_file_stream.flush().unwrap(); - output_list_streams.reflected_file_stream.flush().unwrap(); - output_list_streams.deposited_file_stream.flush().unwrap(); - output_list_streams.sputtered_file_stream.flush().unwrap(); - output_list_streams.trajectory_data_stream.flush().unwrap(); - output_list_streams.trajectory_file_stream.flush().unwrap(); -} +use super::*; +use std::fs::File; + +pub struct OutputUnits { + pub length_unit: f64, + pub energy_unit: f64, + pub mass_unit: f64 +} + +/// Converts from 6D particle coordinates to energy, angle coordinates w.r.t. negative x-axis +pub fn energy_angle_from_particle(particle: &particle::Particle, units: &OutputUnits) -> (f64, f64) { + let energy = particle.E/units.energy_unit; + let ux = particle.dir.x; + let uy = particle.dir.y; + let uz = particle.dir.z; + + let vyz = ((uy).powi(2) + (uz).powi(2)).sqrt(); + let angle = vyz.atan2(-ux) * 180.0 / PI; + + (energy, angle) +} + +#[cfg(feature = "distributions")] +extern crate ndarray; + +#[cfg(feature = "distributions")] +use ndarray::prelude::*; + +/// Distribution tracker for tracking EADs and implantation distributions +#[derive(Serialize)] +#[cfg(feature = "distributions")] +pub struct Distributions { + pub energies: Array1, + pub angles: Array1, + pub x_range: Array1, + pub y_range: Array1, + pub z_range: Array1, + pub reflected_ead: Array2, + pub sputtered_ead: Array2, + pub implanted_x: Array1, + pub implanted_y: Array1, + pub implanted_z: Array1, + pub electronic_energy_loss_x: Array1, + pub nuclear_energy_loss_x: Array1, +} + +#[cfg(feature = "distributions")] +impl Distributions { + pub fn new(options: &Options) -> Distributions { + Distributions { + energies: Array::linspace(options.energy_min, options.energy_max, options.energy_num), + angles: Array::linspace(options.angle_min, options.angle_max, options.angle_num), + x_range: Array::linspace(options.x_min, options.x_max, options.x_num), + y_range: Array::linspace(options.y_min, options.y_max, options.y_num), + z_range: Array::linspace(options.z_min, options.z_max, options.z_num), + reflected_ead: Array::zeros((options.energy_num, options.angle_num)), + sputtered_ead: Array::zeros((options.energy_num, options.angle_num)), + implanted_x: Array::zeros(options.x_num), + implanted_y: Array::zeros(options.y_num), + implanted_z: Array::zeros(options.z_num), + electronic_energy_loss_x: Array::zeros(options.x_num), + nuclear_energy_loss_x: Array::zeros(options.x_num), + } + } + + /// Write distributions to toml + pub fn print(&self, options: &Options) { + + let distribution_output_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "distributions.toml")) + .context("Could not open distributions output file.") + .unwrap(); + let mut distribution_file_stream = BufWriter::with_capacity(8000, distribution_output_file); + let toml = toml::to_string(&self).unwrap(); + writeln!(distribution_file_stream, "{}", toml).unwrap(); + + } + + /// Updates distributions with a single particle + pub fn update(&mut self, particle: &particle::Particle, units: &OutputUnits, options: &Options, num_incident: usize) { + let (energy, angle) = energy_angle_from_particle(particle, units); + + let delta_energy = self.energies[1] - self.energies[0]; + let energy_index_left: i32 = ((energy - self.energies[0])/delta_energy).floor() as i32; + + let delta_angle = self.angles[1] - self.angles[0]; + let angle_index_left: i32 = ((angle - self.angles[0])/delta_angle).floor() as i32; + + let inside_energy = (energy_index_left >= 0) & (energy_index_left < self.energies.len() as i32); + let inside_angle = (angle_index_left >= 0) & (angle_index_left < self.angles.len() as i32); + + if particle.incident & particle.left { + if inside_energy & inside_angle { + self.reflected_ead[[energy_index_left as usize, angle_index_left as usize]] += 1; + } + } + + if !particle.incident & particle.left { + if inside_energy & inside_angle { + self.sputtered_ead[[energy_index_left as usize, angle_index_left as usize]] += 1; + } + } + + if particle.incident & particle.stopped { + let x = particle.pos.x/units.length_unit; + let y = particle.pos.y/units.length_unit; + let z = particle.pos.z/units.length_unit; + + let delta_x = self.x_range[1] - self.x_range[0]; + let delta_y = self.y_range[1] - self.y_range[0]; + let delta_z = self.z_range[1] - self.z_range[0]; + + let x_index_left: i32 = ((x - self.x_range[0])/delta_x) as i32; + let y_index_left: i32 = ((y - self.y_range[0])/delta_y) as i32; + let z_index_left: i32 = ((z - self.z_range[0])/delta_z) as i32; + + let inside_x = (x_index_left >= 0) & (x_index_left < self.x_range.len() as i32); + let inside_y = (y_index_left >= 0) & (y_index_left < self.y_range.len() as i32); + let inside_z = (z_index_left >= 0) & (z_index_left < self.z_range.len() as i32); + + if inside_x { + self.implanted_x[x_index_left as usize] += 1; + } + if inside_y { + self.implanted_y[y_index_left as usize] += 1; + } + if inside_z { + self.implanted_z[z_index_left as usize] += 1; + } + } + + if options.track_energy_losses & particle.incident { + + for energy_loss in particle.energies.iter() { + let x = energy_loss.x/units.length_unit; + let delta_x = self.x_range[1] - self.x_range[0]; + + let electronic_energy_loss = energy_loss.Ee/units.energy_unit/(num_incident as f64)/delta_x; + let nuclear_energy_loss = energy_loss.En/units.energy_unit/(num_incident as f64)/delta_x; + + let x_index_left: i32 = ((x - self.x_range[0])/delta_x) as i32; + let inside_x = (x_index_left >= 0) & (x_index_left < self.x_range.len() as i32); + + if inside_x { + self.electronic_energy_loss_x[x_index_left as usize] += electronic_energy_loss; + self.nuclear_energy_loss_x[x_index_left as usize] += nuclear_energy_loss; + } + } + } + } +} + +/// File streams for list output +pub struct OutputListStreams { + reflected_file_stream: BufWriter, + sputtered_file_stream: BufWriter, + deposited_file_stream: BufWriter, + trajectory_file_stream: BufWriter, + trajectory_data_stream: BufWriter, + displacements_file_stream: BufWriter, + energy_loss_file_stream: BufWriter, +} + +/// Simulation-wide summary output tracker. +pub struct Summary { + pub num_incident: u64, + pub num_sputtered: u64, + pub num_reflected: u64, +} + +/// Summary tracker of sputtering and reflection +impl Summary { + pub fn new(num_incident: u64) -> Summary { + Summary { + num_incident, + num_sputtered: 0, + num_reflected: 0, + } + } + + pub fn add(&mut self, particle: &particle::Particle) { + if particle.incident & particle.left { + self.num_reflected += 1; + } + if !particle.incident & particle.left { + self.num_sputtered += 1; + } + } +} + +pub struct SummaryPerSpecies { + pub m: Vec, + pub sputtered: Vec, + pub reflected: Vec, + pub deposited: Vec, + pub summary_stream_file: BufWriter, +} + +impl SummaryPerSpecies { + pub fn new(options: &Options) -> SummaryPerSpecies { + + let summary_output_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "summary.output")) + .context("Could not open output file.") + .unwrap(); + let writer = BufWriter::with_capacity(8000, summary_output_file); + + SummaryPerSpecies { + m: vec![], + sputtered: vec![], + reflected: vec![], + deposited: vec![], + summary_stream_file: writer, + } + } + + pub fn print(&mut self, options: &Options, output_units: &OutputUnits) { + //Write to summary file + writeln!(self.summary_stream_file, "mass, reflected, sputtered, deposited") + .expect(format!("Output error: could not write to {}summary.output.", options.name).as_str()); + + for (mass, reflected, sputtered, deposited) in izip!(&self.m, &self.reflected, &self.sputtered, &self.deposited) { + writeln!(self.summary_stream_file, "{}, {}, {}, {},", mass/output_units.mass_unit, reflected, sputtered, deposited) + .expect(format!("Output error: could not write to {}summary.output.", options.name).as_str()); + } + self.summary_stream_file.flush().unwrap(); + } + + pub fn update(&mut self, particle: &particle::Particle) { + + if self.m.contains(&(particle.m)) { + + let index = self.m.iter().position(|m| *m == particle.m).unwrap(); + + match (particle.incident, particle.left) { + (true, true) => self.reflected[index] += 1, + (true, false) => self.deposited[index] += 1, + (false, true) => self.sputtered[index] += 1, + _ => (), + } + } else { + self.m.push(particle.m); + match (particle.incident, particle.left) { + (true, true) => {self.reflected.push(1); self.deposited.push(0); self.sputtered.push(0);}, + (true, false) => {self.reflected.push(0); self.deposited.push(1); self.sputtered.push(0);}, + (false, true) => {self.reflected.push(0); self.deposited.push(0); self.sputtered.push(1);}, + _ => {self.reflected.push(0); self.deposited.push(0); self.sputtered.push(0);}, + } + } + } +} + +/// Open list output files for streaming write +pub fn open_output_lists(options: &Options) -> OutputListStreams { + //Open output files for streaming output + let reflected_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "reflected.output")) + .context("Could not open output file.") + .unwrap(); + let reflected_file_stream = BufWriter::with_capacity(options.write_buffer_size, reflected_file); + + let sputtered_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "sputtered.output")) + .context("Could not open output file.") + .unwrap(); + let sputtered_file_stream = BufWriter::with_capacity(options.write_buffer_size, sputtered_file); + + let deposited_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "deposited.output")) + .context("Could not open output file.") + .unwrap(); + let deposited_file_stream = BufWriter::with_capacity(options.write_buffer_size, deposited_file); + + let trajectory_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "trajectories.output")) + .context("Could not open output file.") + .unwrap(); + let trajectory_file_stream = BufWriter::with_capacity(options.write_buffer_size, trajectory_file); + + let trajectory_data = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "trajectory_data.output")) + .context("Could not open output file.") + .unwrap(); + let trajectory_data_stream = BufWriter::with_capacity(options.write_buffer_size, trajectory_data); + + let displacements_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "displacements.output")) + .context("Could not open output file.") + .unwrap(); + let displacements_file_stream = BufWriter::with_capacity(options.write_buffer_size, displacements_file); + + let energy_loss_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(format!("{}{}", options.name, "energy_loss.output")) + .context("Could not open output file.") + .unwrap(); + let energy_loss_file_stream = BufWriter::with_capacity(options.write_buffer_size, energy_loss_file); + + OutputListStreams { + reflected_file_stream, + sputtered_file_stream, + deposited_file_stream, + trajectory_file_stream, + trajectory_data_stream, + displacements_file_stream, + energy_loss_file_stream, + } +} + +/// Write output lists +pub fn output_lists(output_list_streams: &mut OutputListStreams, particle: particle::Particle, options: &Options, output_units: &OutputUnits) { + + let length_unit = output_units.length_unit; + let energy_unit = output_units.energy_unit; + let mass_unit = output_units.mass_unit; + + if !particle.incident & options.track_displacements { + writeln!( + output_list_streams.displacements_file_stream, "{},{},{},{},{},{},{},{},{}", + particle.m/mass_unit, particle.Z, particle.energy_origin/energy_unit, + particle.pos_origin.x/length_unit, particle.pos_origin.y/length_unit, particle.pos_origin.z/length_unit, + particle.pos.x/length_unit, particle.pos.y/length_unit, particle.pos.z/length_unit + ).expect(format!("Output error: could not write to {}displacements.output.", options.name).as_str()); + } + + //Incident particle, left simulation: reflected + if particle.incident & particle.left { + writeln!( + output_list_streams.reflected_file_stream, "{},{},{},{},{},{},{},{},{},{}", + particle.m/mass_unit, particle.Z, particle.E/energy_unit, + particle.pos.x/length_unit, particle.pos.y/length_unit, particle.pos.z/length_unit, + particle.dir.x, particle.dir.y, particle.dir.z, + particle.number_collision_events + ).expect(format!("Output error: could not write to {}reflected.output.", options.name).as_str()); + } + + //Incident particle, stopped in material: deposited + if particle.incident & particle.stopped { + writeln!( + output_list_streams.deposited_file_stream, "{},{},{},{},{},{}", + particle.m/mass_unit, particle.Z, + particle.pos.x/length_unit, particle.pos.y/length_unit, particle.pos.z/length_unit, + particle.number_collision_events + ).expect(format!("Output error: could not write to {}deposited.output.", options.name).as_str()); + } + + //Not an incident particle, left material: sputtered + if !particle.incident & particle.left { + writeln!( + output_list_streams.sputtered_file_stream, "{},{},{},{},{},{},{},{},{},{},{},{},{}", + particle.m/mass_unit, particle.Z, particle.E/energy_unit, + particle.pos.x/length_unit, particle.pos.y/length_unit, particle.pos.z/length_unit, + particle.dir.x, particle.dir.y, particle.dir.z, + particle.number_collision_events, + particle.pos_origin.x/length_unit, particle.pos_origin.y/length_unit, particle.pos_origin.z/length_unit + ).expect(format!("Output error: could not write to {}sputtered.output.", options.name).as_str()); + } + + //Trajectory output + if particle.track_trajectories { + writeln!(output_list_streams.trajectory_data_stream, "{}", particle.trajectory.len()) + .expect(format!("Output error: could not write to {}trajectory_data.output.", options.name).as_str()); + + for pos in particle.trajectory { + writeln!( + output_list_streams.trajectory_file_stream, "{},{},{},{},{},{}", + particle.m/mass_unit, particle.Z, pos.E/energy_unit, + pos.x/length_unit, pos.y/length_unit, pos.z/length_unit, + ).expect(format!("Output error: could not write to {}trajectories.output.", options.name).as_str()); + } + } + + if particle.incident & options.track_energy_losses { + for energy_loss in particle.energies { + writeln!( + output_list_streams.energy_loss_file_stream, "{},{},{},{},{},{},{}", + particle.m/mass_unit, particle.Z, + energy_loss.En/energy_unit, energy_loss.Ee/energy_unit, + energy_loss.x/length_unit, energy_loss.y/length_unit, energy_loss.z/length_unit, + ).expect(format!("Output error: could not write to {}energy_loss.output.", options.name).as_str()); + } + } +} + +/// Flush output list streams +pub fn output_list_flush(output_list_streams: &mut OutputListStreams) { + output_list_streams.displacements_file_stream.flush().unwrap(); + output_list_streams.reflected_file_stream.flush().unwrap(); + output_list_streams.deposited_file_stream.flush().unwrap(); + output_list_streams.sputtered_file_stream.flush().unwrap(); + output_list_streams.trajectory_data_stream.flush().unwrap(); + output_list_streams.trajectory_file_stream.flush().unwrap(); +} diff --git a/src/parry.rs b/src/parry.rs index ffbf6aa..ac99ced 100644 --- a/src/parry.rs +++ b/src/parry.rs @@ -1,292 +1,292 @@ -use super::*; -use parry3d_f64::shape::{Ball, TriMesh}; -use parry3d_f64::query::{PointQuery, Ray, RayCast}; -use parry3d_f64::math::{Isometry, Point, Vector}; -use parry3d_f64::bounding_volume::AABB; - -#[derive(Deserialize, Clone)] -pub struct InputParryBall { - pub options: Options, - pub material_parameters: material::MaterialParameters, - pub particle_parameters: particle::ParticleParameters, - pub geometry_input: ParryBallInput, -} - -impl InputFile for InputParryBall { - - fn new(string: &str) -> InputParryBall { - toml::from_str(string).expect("Could not parse TOML file.") - } - - fn get_options(&self) -> &Options{ - &self.options - } - fn get_material_parameters(&self) -> &material::MaterialParameters{ - &self.material_parameters - } - fn get_particle_parameters(&self) ->& particle::ParticleParameters{ - &self.particle_parameters - } - fn get_geometry_input(&self) -> &Self::GeometryInput{ - &self.geometry_input - } -} - -#[derive(Deserialize, Clone)] -pub struct ParryBallInput { - pub length_unit: String, - pub radius: f64, - pub densities: Vec, - pub electronic_stopping_correction_factor: f64, -} - -#[derive(Clone)] -pub struct ParryBall { - pub densities: Vec, - pub concentrations: Vec, - pub radius: f64, - pub electronic_stopping_correction_factor: f64, - pub energy_barrier_thickness: f64, - pub ball: Ball, -} - -impl GeometryInput for InputParryBall { - type GeometryInput = ParryBallInput; -} - -impl Geometry for ParryBall { - - type InputFileFormat = InputParryBall; - - fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> ParryBall { - - let length_unit: f64 = match input.length_unit.as_str() { - "MICRON" => MICRON, - "CM" => CM, - "ANGSTROM" => ANGSTROM, - "NM" => NM, - "M" => 1., - _ => input.length_unit.parse() - .expect(format!( - "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", - &input.length_unit.as_str() - ).as_str()), - }; - - let electronic_stopping_correction_factor = input.electronic_stopping_correction_factor; - let densities: Vec = input.densities.iter().map(|element| element/(length_unit).powi(3)).collect(); - let total_density: f64 = densities.iter().sum(); - let energy_barrier_thickness = total_density.powf(-1./3.)/SQRTPI*2.; - let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); - let radius = input.radius*length_unit; - - ParryBall { - densities, - concentrations, - radius, - electronic_stopping_correction_factor, - energy_barrier_thickness, - ball: Ball::new(radius) - } - } - - fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { - &self.densities - } - fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { - self.electronic_stopping_correction_factor - } - fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64{ - self.densities.iter().sum() - } - fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { - &self.concentrations - } - fn inside(&self, x: f64, y: f64, z: f64) -> bool { - let p = Point::new(x , y , z ); - self.ball.contains_local_point(&p) - } - - fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { - let p = Point::new(x , y , z ); - (self.ball.distance_to_local_point(&p, true) as f64) < 10.*self.energy_barrier_thickness - } - - fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { - let p = Point::new(x , y , z ); - (self.ball.distance_to_local_point(&p, true) as f64) < self.energy_barrier_thickness - } - - fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { - let p = Point::new(x , y , z ); - let point_projection = self.ball.project_point(&Isometry::identity(), &p, false); - let (x_, y_, z_) = (point_projection.point.x, point_projection.point.y, point_projection.point.z); - (x_ as f64, y_ as f64, z_ as f64) - } -} - - -#[derive(Deserialize, Clone)] -pub struct InputParryTriMesh { - pub options: Options, - pub material_parameters: material::MaterialParameters, - pub particle_parameters: particle::ParticleParameters, - pub geometry_input: ParryTriMeshInput, -} - -impl InputFile for InputParryTriMesh { - - fn new(string: &str) -> InputParryTriMesh { - toml::from_str(string).expect("Could not parse TOML file.") - } - - fn get_options(&self) -> &Options{ - &self.options - } - fn get_material_parameters(&self) -> &material::MaterialParameters{ - &self.material_parameters - } - fn get_particle_parameters(&self) ->& particle::ParticleParameters{ - &self.particle_parameters - } - fn get_geometry_input(&self) -> &Self::GeometryInput{ - &self.geometry_input - } -} - -#[derive(Deserialize, Clone)] -pub struct ParryTriMeshInput { - pub length_unit: String, - pub densities: Vec, - pub electronic_stopping_correction_factor: f64, - pub vertices: Vec<[f64; 3]>, - pub indices: Vec<[u32; 3]>, -} - -#[derive(Clone)] -pub struct ParryTriMesh { - pub densities: Vec, - pub concentrations: Vec, - pub electronic_stopping_correction_factor: f64, - pub energy_barrier_thickness: f64, - pub trimesh: TriMesh, - pub boundary: AABB, -} - -impl GeometryInput for InputParryTriMesh { - type GeometryInput = ParryTriMeshInput; -} - -impl Geometry for ParryTriMesh { - - type InputFileFormat = InputParryTriMesh; - - fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> ParryTriMesh { - - let length_unit: f64 = match input.length_unit.as_str() { - "MICRON" => MICRON, - "CM" => CM, - "ANGSTROM" => ANGSTROM, - "NM" => NM, - "M" => 1., - _ => input.length_unit.parse() - .expect(format!( - "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", - &input.length_unit.as_str() - ).as_str()), - }; - - let electronic_stopping_correction_factor = input.electronic_stopping_correction_factor; - let densities: Vec = input.densities.iter().map(|element| element/(length_unit).powi(3)).collect(); - let total_density: f64 = densities.iter().sum(); - let energy_barrier_thickness = total_density.powf(-1./3.)/SQRTPI*2.; - let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); - let points = input.vertices.iter().map(|p| Point::new(p[0]*length_unit , p[1]*length_unit , p[2]*length_unit)).collect(); - let trimesh = TriMesh::new(points, input.indices.clone()); - let boundary = trimesh.aabb(&Isometry::identity()); - - ParryTriMesh { - densities, - concentrations, - electronic_stopping_correction_factor, - energy_barrier_thickness, - trimesh, - boundary, - } - } - - fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { - &self.densities - } - fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { - self.electronic_stopping_correction_factor - } - fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64{ - self.densities.iter().sum() - } - fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { - &self.concentrations - } - fn inside(&self, x: f64, y: f64, z: f64) -> bool { - inside_trimesh(&self.trimesh, x, y, z) - } - - fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { - if self.inside(x, y, z) { - true - } else { - let p = Point::new(x , y , z ); - let distance = self.boundary.distance_to_local_point(&p, true); - //dbg!(distance); - distance < 10.*self.energy_barrier_thickness - } - } - - fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { - if self.inside(x, y, z) { - true - } else { - let p = Point::new(x , y , z ); - let distance = self.trimesh.distance_to_local_point(&p, true); - //dbg!(distance/ANGSTROM); - //dbg!(self.energy_barrier_thickness/ANGSTROM); - //dbg!(distance < self.energy_barrier_thickness); - distance < self.energy_barrier_thickness - } - } - - fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { - let p = Point::new(x , y , z ); - let point_projection = self.trimesh.project_local_point(&p, false); - let (x_, y_, z_) = (point_projection.point.x, point_projection.point.y, point_projection.point.z); - (x_ as f64, y_ as f64, z_ as f64) - } -} - -fn inside_trimesh(trimesh: &TriMesh, x: f64, y: f64, z: f64) -> bool { - let p = Point::new(x , y , z ); - - if !trimesh.aabb(&Isometry::identity()) - .contains_local_point(&p) { - return false; - } - - let dir = Vector::new(1.0, 0.0, 0.0); - - let result = trimesh.cast_ray_and_get_normal( - &Isometry::identity(), - &Ray::new(p, dir), - 1.0, - false - ); - - //dbg!(result); - - match result { - Some(intersection) => { - //dbg!(intersection.feature); - trimesh.is_backface(intersection.feature) - }, - None => false, - } -} +use super::*; +use parry3d_f64::shape::{Ball, TriMesh}; +use parry3d_f64::query::{PointQuery, Ray, RayCast}; +use parry3d_f64::math::{Isometry, Point, Vector}; +use parry3d_f64::bounding_volume::AABB; + +#[derive(Deserialize, Clone)] +pub struct InputParryBall { + pub options: Options, + pub material_parameters: material::MaterialParameters, + pub particle_parameters: particle::ParticleParameters, + pub geometry_input: ParryBallInput, +} + +impl InputFile for InputParryBall { + + fn new(string: &str) -> InputParryBall { + toml::from_str(string).expect("Could not parse TOML file.") + } + + fn get_options(&self) -> &Options{ + &self.options + } + fn get_material_parameters(&self) -> &material::MaterialParameters{ + &self.material_parameters + } + fn get_particle_parameters(&self) ->& particle::ParticleParameters{ + &self.particle_parameters + } + fn get_geometry_input(&self) -> &Self::GeometryInput{ + &self.geometry_input + } +} + +#[derive(Deserialize, Clone)] +pub struct ParryBallInput { + pub length_unit: String, + pub radius: f64, + pub densities: Vec, + pub electronic_stopping_correction_factor: f64, +} + +#[derive(Clone)] +pub struct ParryBall { + pub densities: Vec, + pub concentrations: Vec, + pub radius: f64, + pub electronic_stopping_correction_factor: f64, + pub energy_barrier_thickness: f64, + pub ball: Ball, +} + +impl GeometryInput for InputParryBall { + type GeometryInput = ParryBallInput; +} + +impl Geometry for ParryBall { + + type InputFileFormat = InputParryBall; + + fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> ParryBall { + + let length_unit: f64 = match input.length_unit.as_str() { + "MICRON" => MICRON, + "CM" => CM, + "ANGSTROM" => ANGSTROM, + "NM" => NM, + "M" => 1., + _ => input.length_unit.parse() + .expect(format!( + "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", + &input.length_unit.as_str() + ).as_str()), + }; + + let electronic_stopping_correction_factor = input.electronic_stopping_correction_factor; + let densities: Vec = input.densities.iter().map(|element| element/(length_unit).powi(3)).collect(); + let total_density: f64 = densities.iter().sum(); + let energy_barrier_thickness = total_density.powf(-1./3.)/SQRTPI*2.; + let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); + let radius = input.radius*length_unit; + + ParryBall { + densities, + concentrations, + radius, + electronic_stopping_correction_factor, + energy_barrier_thickness, + ball: Ball::new(radius) + } + } + + fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { + &self.densities + } + fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { + self.electronic_stopping_correction_factor + } + fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64{ + self.densities.iter().sum() + } + fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { + &self.concentrations + } + fn inside(&self, x: f64, y: f64, z: f64) -> bool { + let p = Point::new(x , y , z ); + self.ball.contains_local_point(&p) + } + + fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { + let p = Point::new(x , y , z ); + (self.ball.distance_to_local_point(&p, true) as f64) < 10.*self.energy_barrier_thickness + } + + fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { + let p = Point::new(x , y , z ); + (self.ball.distance_to_local_point(&p, true) as f64) < self.energy_barrier_thickness + } + + fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { + let p = Point::new(x , y , z ); + let point_projection = self.ball.project_point(&Isometry::identity(), &p, false); + let (x_, y_, z_) = (point_projection.point.x, point_projection.point.y, point_projection.point.z); + (x_ as f64, y_ as f64, z_ as f64) + } +} + + +#[derive(Deserialize, Clone)] +pub struct InputParryTriMesh { + pub options: Options, + pub material_parameters: material::MaterialParameters, + pub particle_parameters: particle::ParticleParameters, + pub geometry_input: ParryTriMeshInput, +} + +impl InputFile for InputParryTriMesh { + + fn new(string: &str) -> InputParryTriMesh { + toml::from_str(string).expect("Could not parse TOML file.") + } + + fn get_options(&self) -> &Options{ + &self.options + } + fn get_material_parameters(&self) -> &material::MaterialParameters{ + &self.material_parameters + } + fn get_particle_parameters(&self) ->& particle::ParticleParameters{ + &self.particle_parameters + } + fn get_geometry_input(&self) -> &Self::GeometryInput{ + &self.geometry_input + } +} + +#[derive(Deserialize, Clone)] +pub struct ParryTriMeshInput { + pub length_unit: String, + pub densities: Vec, + pub electronic_stopping_correction_factor: f64, + pub vertices: Vec<[f64; 3]>, + pub indices: Vec<[u32; 3]>, +} + +#[derive(Clone)] +pub struct ParryTriMesh { + pub densities: Vec, + pub concentrations: Vec, + pub electronic_stopping_correction_factor: f64, + pub energy_barrier_thickness: f64, + pub trimesh: TriMesh, + pub boundary: AABB, +} + +impl GeometryInput for InputParryTriMesh { + type GeometryInput = ParryTriMeshInput; +} + +impl Geometry for ParryTriMesh { + + type InputFileFormat = InputParryTriMesh; + + fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> ParryTriMesh { + + let length_unit: f64 = match input.length_unit.as_str() { + "MICRON" => MICRON, + "CM" => CM, + "ANGSTROM" => ANGSTROM, + "NM" => NM, + "M" => 1., + _ => input.length_unit.parse() + .expect(format!( + "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", + &input.length_unit.as_str() + ).as_str()), + }; + + let electronic_stopping_correction_factor = input.electronic_stopping_correction_factor; + let densities: Vec = input.densities.iter().map(|element| element/(length_unit).powi(3)).collect(); + let total_density: f64 = densities.iter().sum(); + let energy_barrier_thickness = total_density.powf(-1./3.)/SQRTPI*2.; + let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); + let points = input.vertices.iter().map(|p| Point::new(p[0]*length_unit , p[1]*length_unit , p[2]*length_unit)).collect(); + let trimesh = TriMesh::new(points, input.indices.clone()); + let boundary = trimesh.aabb(&Isometry::identity()); + + ParryTriMesh { + densities, + concentrations, + electronic_stopping_correction_factor, + energy_barrier_thickness, + trimesh, + boundary, + } + } + + fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { + &self.densities + } + fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { + self.electronic_stopping_correction_factor + } + fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64{ + self.densities.iter().sum() + } + fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { + &self.concentrations + } + fn inside(&self, x: f64, y: f64, z: f64) -> bool { + inside_trimesh(&self.trimesh, x, y, z) + } + + fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { + if self.inside(x, y, z) { + true + } else { + let p = Point::new(x , y , z ); + let distance = self.boundary.distance_to_local_point(&p, true); + //dbg!(distance); + distance < 10.*self.energy_barrier_thickness + } + } + + fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { + if self.inside(x, y, z) { + true + } else { + let p = Point::new(x , y , z ); + let distance = self.trimesh.distance_to_local_point(&p, true); + //dbg!(distance/ANGSTROM); + //dbg!(self.energy_barrier_thickness/ANGSTROM); + //dbg!(distance < self.energy_barrier_thickness); + distance < self.energy_barrier_thickness + } + } + + fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { + let p = Point::new(x , y , z ); + let point_projection = self.trimesh.project_local_point(&p, false); + let (x_, y_, z_) = (point_projection.point.x, point_projection.point.y, point_projection.point.z); + (x_ as f64, y_ as f64, z_ as f64) + } +} + +fn inside_trimesh(trimesh: &TriMesh, x: f64, y: f64, z: f64) -> bool { + let p = Point::new(x , y , z ); + + if !trimesh.aabb(&Isometry::identity()) + .contains_local_point(&p) { + return false; + } + + let dir = Vector::new(1.0, 0.0, 0.0); + + let result = trimesh.cast_ray_and_get_normal( + &Isometry::identity(), + &Ray::new(p, dir), + 1.0, + false + ); + + //dbg!(result); + + match result { + Some(intersection) => { + //dbg!(intersection.feature); + trimesh.is_backface(intersection.feature) + }, + None => false, + } +} diff --git a/src/particle.rs b/src/particle.rs index 51e8eb1..7c73721 100644 --- a/src/particle.rs +++ b/src/particle.rs @@ -1,258 +1,258 @@ -use super::*; - -/// Rustbca's internal representation of the particle_parameters input. - - -#[cfg(feature = "hdf5_input")] -#[derive(Deserialize, Clone)] -pub struct ParticleParameters { - pub particle_input_filename: String, - pub length_unit: String, - pub energy_unit: String, - pub mass_unit: String, - pub N: Vec, - pub m: Vec, - pub Z: Vec, - pub E: Vec, - pub Ec: Vec, - pub Es: Vec, - pub pos: Vec<(f64, f64, f64)>, - pub dir: Vec<(f64, f64, f64)>, - pub interaction_index: Vec -} - -#[cfg(not(feature = "hdf5_input"))] -#[derive(Deserialize, Clone)] -pub struct ParticleParameters { - pub length_unit: String, - pub energy_unit: String, - pub mass_unit: String, - pub N: Vec, - pub m: Vec, - pub Z: Vec, - pub E: Vec, - pub Ec: Vec, - pub Es: Vec, - pub pos: Vec<(f64, f64, f64)>, - pub dir: Vec<(f64, f64, f64)>, - pub interaction_index: Vec -} - -/// HDF5 version of particle input. -#[derive(Clone, PartialEq, Debug, Copy)] -#[cfg_attr(feature = "hdf5_input", derive(hdf5::H5Type))] -#[repr(C)] -pub struct ParticleInput { - pub m: f64, - pub Z: f64, - pub E: f64, - pub Ec: f64, - pub Es: f64, - pub x: f64, - pub y: f64, - pub z: f64, - pub ux: f64, - pub uy: f64, - pub uz: f64, - pub interaction_index: usize, -} - -/// Particle object. Particles in rustbca include incident ions and material atoms. -#[derive(Clone)] -pub struct Particle { - pub m: f64, - pub Z: f64, - pub E: f64, - pub Ec: f64, - pub Es: f64, - pub pos: Vector, - pub dir: Vector, - pub pos_old: Vector, - pub dir_old: Vector, - pub pos_origin: Vector, - pub energy_origin: f64, - pub asymptotic_deflection: f64, - pub stopped: bool, - pub left: bool, - pub incident: bool, - pub first_step: bool, - pub trajectory: Vec, - pub energies: Vec, - pub track_trajectories: bool, - pub number_collision_events: usize, - pub backreflected: bool, - pub interaction_index: usize, -} -impl Particle { - /// Construct a particle object from input. - pub fn from_input(input: ParticleInput, options: &Options) -> Particle { - let dirx = input.ux; - let diry = input.uy; - let dirz = input.uz; - - let dir_mag = (dirx*dirx + diry*diry + dirz*dirz).sqrt(); - - assert!((dirx/dir_mag).abs() < 1.0 - f64::EPSILON, "Input error: incident direction cannot round to exactly (1, 0, 0) due to gimbal lock. Use a non-zero y-component."); - assert!(input.E > 0., "Input error: incident energy {}; must be greater than zero.", input.E/EV); - - Particle { - m: input.m, - Z: input.Z, - E: input.E, - Ec: input.Ec, - Es: input.Es, - pos: Vector::new(input.x, input.y, input.z), - dir: Vector::new(dirx/dir_mag, diry/dir_mag, dirz/dir_mag), - pos_old: Vector::new(input.x, input.y, input.z), - dir_old: Vector::new(dirx/dir_mag, diry/dir_mag, dirz/dir_mag), - pos_origin: Vector::new(input.x, input.y, input.z), - energy_origin: input.E, - asymptotic_deflection: 0., - stopped: false, - left: false, - incident: true, - first_step: true, - trajectory: vec![Vector4::new(input.E, input.x, input.y, input.z)], - energies: vec![], - track_trajectories: options.track_trajectories, - number_collision_events: 0, - backreflected: false, - interaction_index: input.interaction_index, - } - } - - /// Particle constructor from raw inputs. - pub fn new(m: f64, Z: f64, E: f64, Ec: f64, Es: f64, x: f64, y: f64, z: f64, dirx: f64, diry: f64, dirz: f64, incident: bool, track_trajectories: bool, interaction_index: usize) -> Particle { - let dir_mag = (dirx*dirx + diry*diry + dirz*dirz).sqrt(); - - Particle { - m, - Z, - E, - Ec, - Es, - pos: Vector::new(x, y, z), - dir: Vector::new(dirx/dir_mag, diry/dir_mag, dirz/dir_mag), - pos_old: Vector::new(x, y, z), - dir_old: Vector::new(dirx/dir_mag, diry/dir_mag, dirz/dir_mag), - pos_origin: Vector::new(x, y, z), - energy_origin: E, - asymptotic_deflection: 0., - stopped: false, - left: false, - incident, - first_step: incident, - trajectory: vec![], - energies: vec![EnergyLoss::new(0., 0., x, y, z)], - track_trajectories, - number_collision_events: 0, - backreflected: false, - interaction_index, - } - } - - /// If `track_trajectories`, add the current (E, x, y, z) to the trajectory. - pub fn add_trajectory(&mut self) { - if self.track_trajectories { - self.trajectory.push(Vector4 {E: self.E, x: self.pos.x, y: self.pos.y, z: self.pos.z}); - } - } - - /// If `track_energy_losses`, add the most recent electronic and nuclear energy loss terms and (x, y, z) to the energy loss tracker. - pub fn energy_loss(&mut self, options: &Options, En: f64, Ee: f64) { - if self.incident & options.track_energy_losses { - self.energies.push(EnergyLoss {Ee, En, x: self.pos.x, y: self.pos.y, z: self.pos.z}); - } - } - - /// Get the current momentum. - pub fn get_momentum(&mut self) -> Vector { - let speed = (2.*self.E/self.m).sqrt(); - Vector::new( - self.m*speed*self.dir.x, - self.m*speed*self.dir.y, - self.m*speed*self.dir.z, - ) - } -} - -/// Rotate a particle by a deflection psi at an azimuthal angle phi. -pub fn rotate_particle(particle_1: &mut particle::Particle, psi: f64, phi: f64) { - let cosx: f64 = particle_1.dir.x; - let cosy: f64 = particle_1.dir.y; - let cosz: f64 = particle_1.dir.z; - let cphi: f64 = phi.cos(); - let sphi: f64 = phi.sin(); - let sa = (1. - cosx*cosx).sqrt(); - - //Particle direction update formulas from original TRIDYN paper, see Moeller and Eckstein 1988 - let cpsi: f64 = psi.cos(); - let spsi: f64 = psi.sin(); - let cosx_new: f64 = cpsi*cosx + spsi*cphi*sa; - let cosy_new: f64 = cpsi*cosy - spsi/sa*(cphi*cosx*cosy - sphi*cosz); - let cosz_new: f64 = cpsi*cosz - spsi/sa*(cphi*cosx*cosz + sphi*cosy); - - let dir_new = Vector {x: cosx_new, y: cosy_new, z: cosz_new}; - - particle_1.dir.assign(&dir_new); - particle_1.dir.normalize(); -} - -/// Push particle in space according to previous direction and return the distance traveled. -pub fn particle_advance(particle_1: &mut particle::Particle, mfp: f64, asymptotic_deflection: f64) -> f64 { - - if particle_1.E > particle_1.Ec { - particle_1.add_trajectory(); - } - - //Update previous position - particle_1.pos_old.x = particle_1.pos.x; - particle_1.pos_old.y = particle_1.pos.y; - particle_1.pos_old.z = particle_1.pos.z; - - //In order to keep average denisty constant, must add back previous asymptotic deflection - let distance_traveled = mfp + particle_1.asymptotic_deflection - asymptotic_deflection; - //let distance_traveled = mfp - asymptotic_deflection; - - //dir has been updated, so use previous direction to advance in space - particle_1.pos.x += particle_1.dir_old.x*distance_traveled; - particle_1.pos.y += particle_1.dir_old.y*distance_traveled; - particle_1.pos.z += particle_1.dir_old.z*distance_traveled; - particle_1.asymptotic_deflection = asymptotic_deflection; - - //Update previous direction - particle_1.dir_old.x = particle_1.dir.x; - particle_1.dir_old.y = particle_1.dir.y; - particle_1.dir_old.z = particle_1.dir.z; - - return distance_traveled; -} - -pub fn surface_refraction(particle: &mut Particle, normal: Vector, Es: f64) { - let E = particle.E; - - let costheta = particle.dir.dot(&normal); - - let a = (E/(E + Es)).sqrt(); - let b = -(E).sqrt()*costheta; - let c = (E*costheta.powi(2) + Es).sqrt(); - - let u1x = (E/(E + Es)).sqrt()*particle.dir.x + ((-(E).sqrt()*costheta + (E*costheta.powi(2) + Es).sqrt())/(E + Es).sqrt())*normal.x; - let u1y = (E/(E + Es)).sqrt()*particle.dir.y + ((-(E).sqrt()*costheta + (E*costheta.powi(2) + Es).sqrt())/(E + Es).sqrt())*normal.y; - let u1z = (E/(E + Es)).sqrt()*particle.dir.z + ((-(E).sqrt()*costheta + (E*costheta.powi(2) + Es).sqrt())/(E + Es).sqrt())*normal.z; - particle.dir.x = u1x; - particle.dir.y = u1y; - particle.dir.z = u1z; - particle.E += Es; -} - -/// Calcualte the refraction angle based on the surface binding energy of the material. -pub fn refraction_angle(costheta: f64, energy_old: f64, energy_new: f64) -> f64 { - let costheta = if costheta.abs() > 1. {costheta.signum()} else {costheta}; - let sintheta0 = (1. - costheta*costheta).sqrt(); - let sintheta1 = sintheta0*(energy_old/energy_new).sqrt(); - let delta_theta = sintheta1.asin() - sintheta0.asin(); - assert!(!delta_theta.is_nan(), "Numerical error: refraction returned NaN."); - let sign = -costheta.signum(); - return sign*delta_theta; -} +use super::*; + +/// Rustbca's internal representation of the particle_parameters input. + + +#[cfg(feature = "hdf5_input")] +#[derive(Deserialize, Clone)] +pub struct ParticleParameters { + pub particle_input_filename: String, + pub length_unit: String, + pub energy_unit: String, + pub mass_unit: String, + pub N: Vec, + pub m: Vec, + pub Z: Vec, + pub E: Vec, + pub Ec: Vec, + pub Es: Vec, + pub pos: Vec<(f64, f64, f64)>, + pub dir: Vec<(f64, f64, f64)>, + pub interaction_index: Vec +} + +#[cfg(not(feature = "hdf5_input"))] +#[derive(Deserialize, Clone)] +pub struct ParticleParameters { + pub length_unit: String, + pub energy_unit: String, + pub mass_unit: String, + pub N: Vec, + pub m: Vec, + pub Z: Vec, + pub E: Vec, + pub Ec: Vec, + pub Es: Vec, + pub pos: Vec<(f64, f64, f64)>, + pub dir: Vec<(f64, f64, f64)>, + pub interaction_index: Vec +} + +/// HDF5 version of particle input. +#[derive(Clone, PartialEq, Debug, Copy)] +#[cfg_attr(feature = "hdf5_input", derive(hdf5::H5Type))] +#[repr(C)] +pub struct ParticleInput { + pub m: f64, + pub Z: f64, + pub E: f64, + pub Ec: f64, + pub Es: f64, + pub x: f64, + pub y: f64, + pub z: f64, + pub ux: f64, + pub uy: f64, + pub uz: f64, + pub interaction_index: usize, +} + +/// Particle object. Particles in rustbca include incident ions and material atoms. +#[derive(Clone)] +pub struct Particle { + pub m: f64, + pub Z: f64, + pub E: f64, + pub Ec: f64, + pub Es: f64, + pub pos: Vector, + pub dir: Vector, + pub pos_old: Vector, + pub dir_old: Vector, + pub pos_origin: Vector, + pub energy_origin: f64, + pub asymptotic_deflection: f64, + pub stopped: bool, + pub left: bool, + pub incident: bool, + pub first_step: bool, + pub trajectory: Vec, + pub energies: Vec, + pub track_trajectories: bool, + pub number_collision_events: usize, + pub backreflected: bool, + pub interaction_index: usize, +} +impl Particle { + /// Construct a particle object from input. + pub fn from_input(input: ParticleInput, options: &Options) -> Particle { + let dirx = input.ux; + let diry = input.uy; + let dirz = input.uz; + + let dir_mag = (dirx*dirx + diry*diry + dirz*dirz).sqrt(); + + assert!((dirx/dir_mag).abs() < 1.0 - f64::EPSILON, "Input error: incident direction cannot round to exactly (1, 0, 0) due to gimbal lock. Use a non-zero y-component."); + assert!(input.E > 0., "Input error: incident energy {}; must be greater than zero.", input.E/EV); + + Particle { + m: input.m, + Z: input.Z, + E: input.E, + Ec: input.Ec, + Es: input.Es, + pos: Vector::new(input.x, input.y, input.z), + dir: Vector::new(dirx/dir_mag, diry/dir_mag, dirz/dir_mag), + pos_old: Vector::new(input.x, input.y, input.z), + dir_old: Vector::new(dirx/dir_mag, diry/dir_mag, dirz/dir_mag), + pos_origin: Vector::new(input.x, input.y, input.z), + energy_origin: input.E, + asymptotic_deflection: 0., + stopped: false, + left: false, + incident: true, + first_step: true, + trajectory: vec![Vector4::new(input.E, input.x, input.y, input.z)], + energies: vec![], + track_trajectories: options.track_trajectories, + number_collision_events: 0, + backreflected: false, + interaction_index: input.interaction_index, + } + } + + /// Particle constructor from raw inputs. + pub fn new(m: f64, Z: f64, E: f64, Ec: f64, Es: f64, x: f64, y: f64, z: f64, dirx: f64, diry: f64, dirz: f64, incident: bool, track_trajectories: bool, interaction_index: usize) -> Particle { + let dir_mag = (dirx*dirx + diry*diry + dirz*dirz).sqrt(); + + Particle { + m, + Z, + E, + Ec, + Es, + pos: Vector::new(x, y, z), + dir: Vector::new(dirx/dir_mag, diry/dir_mag, dirz/dir_mag), + pos_old: Vector::new(x, y, z), + dir_old: Vector::new(dirx/dir_mag, diry/dir_mag, dirz/dir_mag), + pos_origin: Vector::new(x, y, z), + energy_origin: E, + asymptotic_deflection: 0., + stopped: false, + left: false, + incident, + first_step: incident, + trajectory: vec![], + energies: vec![EnergyLoss::new(0., 0., x, y, z)], + track_trajectories, + number_collision_events: 0, + backreflected: false, + interaction_index, + } + } + + /// If `track_trajectories`, add the current (E, x, y, z) to the trajectory. + pub fn add_trajectory(&mut self) { + if self.track_trajectories { + self.trajectory.push(Vector4 {E: self.E, x: self.pos.x, y: self.pos.y, z: self.pos.z}); + } + } + + /// If `track_energy_losses`, add the most recent electronic and nuclear energy loss terms and (x, y, z) to the energy loss tracker. + pub fn energy_loss(&mut self, options: &Options, En: f64, Ee: f64) { + if self.incident & options.track_energy_losses { + self.energies.push(EnergyLoss {Ee, En, x: self.pos.x, y: self.pos.y, z: self.pos.z}); + } + } + + /// Get the current momentum. + pub fn get_momentum(&mut self) -> Vector { + let speed = (2.*self.E/self.m).sqrt(); + Vector::new( + self.m*speed*self.dir.x, + self.m*speed*self.dir.y, + self.m*speed*self.dir.z, + ) + } +} + +/// Rotate a particle by a deflection psi at an azimuthal angle phi. +pub fn rotate_particle(particle_1: &mut particle::Particle, psi: f64, phi: f64) { + let cosx: f64 = particle_1.dir.x; + let cosy: f64 = particle_1.dir.y; + let cosz: f64 = particle_1.dir.z; + let cphi: f64 = phi.cos(); + let sphi: f64 = phi.sin(); + let sa = (1. - cosx*cosx).sqrt(); + + //Particle direction update formulas from original TRIDYN paper, see Moeller and Eckstein 1988 + let cpsi: f64 = psi.cos(); + let spsi: f64 = psi.sin(); + let cosx_new: f64 = cpsi*cosx + spsi*cphi*sa; + let cosy_new: f64 = cpsi*cosy - spsi/sa*(cphi*cosx*cosy - sphi*cosz); + let cosz_new: f64 = cpsi*cosz - spsi/sa*(cphi*cosx*cosz + sphi*cosy); + + let dir_new = Vector {x: cosx_new, y: cosy_new, z: cosz_new}; + + particle_1.dir.assign(&dir_new); + particle_1.dir.normalize(); +} + +/// Push particle in space according to previous direction and return the distance traveled. +pub fn particle_advance(particle_1: &mut particle::Particle, mfp: f64, asymptotic_deflection: f64) -> f64 { + + if particle_1.E > particle_1.Ec { + particle_1.add_trajectory(); + } + + //Update previous position + particle_1.pos_old.x = particle_1.pos.x; + particle_1.pos_old.y = particle_1.pos.y; + particle_1.pos_old.z = particle_1.pos.z; + + //In order to keep average denisty constant, must add back previous asymptotic deflection + let distance_traveled = mfp + particle_1.asymptotic_deflection - asymptotic_deflection; + //let distance_traveled = mfp - asymptotic_deflection; + + //dir has been updated, so use previous direction to advance in space + particle_1.pos.x += particle_1.dir_old.x*distance_traveled; + particle_1.pos.y += particle_1.dir_old.y*distance_traveled; + particle_1.pos.z += particle_1.dir_old.z*distance_traveled; + particle_1.asymptotic_deflection = asymptotic_deflection; + + //Update previous direction + particle_1.dir_old.x = particle_1.dir.x; + particle_1.dir_old.y = particle_1.dir.y; + particle_1.dir_old.z = particle_1.dir.z; + + return distance_traveled; +} + +pub fn surface_refraction(particle: &mut Particle, normal: Vector, Es: f64) { + let E = particle.E; + + let costheta = particle.dir.dot(&normal); + + let a = (E/(E + Es)).sqrt(); + let b = -(E).sqrt()*costheta; + let c = (E*costheta.powi(2) + Es).sqrt(); + + let u1x = (E/(E + Es)).sqrt()*particle.dir.x + ((-(E).sqrt()*costheta + (E*costheta.powi(2) + Es).sqrt())/(E + Es).sqrt())*normal.x; + let u1y = (E/(E + Es)).sqrt()*particle.dir.y + ((-(E).sqrt()*costheta + (E*costheta.powi(2) + Es).sqrt())/(E + Es).sqrt())*normal.y; + let u1z = (E/(E + Es)).sqrt()*particle.dir.z + ((-(E).sqrt()*costheta + (E*costheta.powi(2) + Es).sqrt())/(E + Es).sqrt())*normal.z; + particle.dir.x = u1x; + particle.dir.y = u1y; + particle.dir.z = u1z; + particle.E += Es; +} + +/// Calcualte the refraction angle based on the surface binding energy of the material. +pub fn refraction_angle(costheta: f64, energy_old: f64, energy_new: f64) -> f64 { + let costheta = if costheta.abs() > 1. {costheta.signum()} else {costheta}; + let sintheta0 = (1. - costheta*costheta).sqrt(); + let sintheta1 = sintheta0*(energy_old/energy_new).sqrt(); + let delta_theta = sintheta1.asin() - sintheta0.asin(); + assert!(!delta_theta.is_nan(), "Numerical error: refraction returned NaN."); + let sign = -costheta.signum(); + return sign*delta_theta; +} diff --git a/src/sphere.rs b/src/sphere.rs index e5bd862..80034cb 100644 --- a/src/sphere.rs +++ b/src/sphere.rs @@ -1,122 +1,122 @@ -use super::*; - -#[derive(Deserialize, Clone)] -pub struct InputSphere { - pub options: Options, - pub material_parameters: material::MaterialParameters, - pub particle_parameters: particle::ParticleParameters, - pub geometry_input: sphere::SphereInput, -} - -impl InputFile for InputSphere { - - fn new(string: &str) -> InputSphere { - toml::from_str(string).expect("Could not parse TOML file.") - } - - fn get_options(&self) -> &Options{ - &self.options - } - fn get_material_parameters(&self) -> &material::MaterialParameters{ - &self.material_parameters - } - fn get_particle_parameters(&self) ->& particle::ParticleParameters{ - &self.particle_parameters - } - fn get_geometry_input(&self) -> &Self::GeometryInput{ - &self.geometry_input - } -} - -#[derive(Deserialize, Clone)] -pub struct SphereInput { - pub length_unit: String, - pub radius: f64, - pub densities: Vec, - pub electronic_stopping_correction_factor: f64, -} - -#[derive(Clone)] -pub struct Sphere { - pub densities: Vec, - pub concentrations: Vec, - pub radius: f64, - pub electronic_stopping_correction_factor: f64, - pub energy_barrier_thickness: f64, -} - -impl GeometryInput for InputSphere { - type GeometryInput = SphereInput; -} - -impl Geometry for Sphere { - - type InputFileFormat = InputSphere; - - fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Sphere { - - let length_unit: f64 = match input.length_unit.as_str() { - "MICRON" => MICRON, - "CM" => CM, - "ANGSTROM" => ANGSTROM, - "NM" => NM, - "M" => 1., - _ => input.length_unit.parse() - .expect(format!( - "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", - &input.length_unit.as_str() - ).as_str()), - }; - - let electronic_stopping_correction_factor = input.electronic_stopping_correction_factor; - let densities: Vec = input.densities.iter().map(|element| element/(length_unit).powi(3)).collect(); - let total_density: f64 = densities.iter().sum(); - let energy_barrier_thickness = total_density.powf(-1./3.)/SQRTPI*2.; - let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); - let radius = input.radius*length_unit; - - Sphere { - densities, - concentrations, - radius, - electronic_stopping_correction_factor, - energy_barrier_thickness, - } - } - - fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { - &self.densities - } - fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { - self.electronic_stopping_correction_factor - } - fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64{ - self.densities.iter().sum() - } - fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { - &self.concentrations - } - fn inside(&self, x: f64, y: f64, z: f64) -> bool { - let r = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); - r < self.radius - } - - fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { - let r = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); - r < (10.*self.energy_barrier_thickness + self.radius) - } - - fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { - let r = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); - r < (self.energy_barrier_thickness + self.radius) - } - - fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { - let r = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); - let R = self.radius; - let ux = x/r; - let uy = y/r; - let uz = z/r; - (ux*R, uy*R, uz*R) - } -} +use super::*; + +#[derive(Deserialize, Clone)] +pub struct InputSphere { + pub options: Options, + pub material_parameters: material::MaterialParameters, + pub particle_parameters: particle::ParticleParameters, + pub geometry_input: sphere::SphereInput, +} + +impl InputFile for InputSphere { + + fn new(string: &str) -> InputSphere { + toml::from_str(string).expect("Could not parse TOML file.") + } + + fn get_options(&self) -> &Options{ + &self.options + } + fn get_material_parameters(&self) -> &material::MaterialParameters{ + &self.material_parameters + } + fn get_particle_parameters(&self) ->& particle::ParticleParameters{ + &self.particle_parameters + } + fn get_geometry_input(&self) -> &Self::GeometryInput{ + &self.geometry_input + } +} + +#[derive(Deserialize, Clone)] +pub struct SphereInput { + pub length_unit: String, + pub radius: f64, + pub densities: Vec, + pub electronic_stopping_correction_factor: f64, +} + +#[derive(Clone)] +pub struct Sphere { + pub densities: Vec, + pub concentrations: Vec, + pub radius: f64, + pub electronic_stopping_correction_factor: f64, + pub energy_barrier_thickness: f64, +} + +impl GeometryInput for InputSphere { + type GeometryInput = SphereInput; +} + +impl Geometry for Sphere { + + type InputFileFormat = InputSphere; + + fn new(input: &<::InputFileFormat as GeometryInput>::GeometryInput) -> Sphere { + + let length_unit: f64 = match input.length_unit.as_str() { + "MICRON" => MICRON, + "CM" => CM, + "ANGSTROM" => ANGSTROM, + "NM" => NM, + "M" => 1., + _ => input.length_unit.parse() + .expect(format!( + "Input errror: could nor parse length unit {}. Use a valid float or one of ANGSTROM, NM, MICRON, CM, MM, M", + &input.length_unit.as_str() + ).as_str()), + }; + + let electronic_stopping_correction_factor = input.electronic_stopping_correction_factor; + let densities: Vec = input.densities.iter().map(|element| element/(length_unit).powi(3)).collect(); + let total_density: f64 = densities.iter().sum(); + let energy_barrier_thickness = total_density.powf(-1./3.)/SQRTPI*2.; + let concentrations: Vec = densities.iter().map(|&density| density/total_density).collect::>(); + let radius = input.radius*length_unit; + + Sphere { + densities, + concentrations, + radius, + electronic_stopping_correction_factor, + energy_barrier_thickness, + } + } + + fn get_densities(&self, x: f64, y: f64, z: f64) -> &Vec { + &self.densities + } + fn get_ck(&self, x: f64, y: f64, z: f64) -> f64 { + self.electronic_stopping_correction_factor + } + fn get_total_density(&self, x: f64, y: f64, z: f64) -> f64{ + self.densities.iter().sum() + } + fn get_concentrations(&self, x: f64, y: f64, z: f64) -> &Vec { + &self.concentrations + } + fn inside(&self, x: f64, y: f64, z: f64) -> bool { + let r = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); + r < self.radius + } + + fn inside_simulation_boundary(&self, x: f64, y: f64, z: f64) -> bool { + let r = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); + r < (10.*self.energy_barrier_thickness + self.radius) + } + + fn inside_energy_barrier(&self, x: f64, y: f64, z: f64) -> bool { + let r = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); + r < (self.energy_barrier_thickness + self.radius) + } + + fn closest_point(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) { + let r = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); + let R = self.radius; + let ux = x/r; + let uy = y/r; + let uz = z/r; + (ux*R, uy*R, uz*R) + } +} diff --git a/src/structs.rs b/src/structs.rs index 2505da7..8aa611a 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,87 +1,87 @@ -/// 3D vector. -#[derive(Clone)] -pub struct Vector { - pub x: f64, - pub y: f64, - pub z: f64, -} -impl Vector { - pub fn new(x: f64, y: f64, z: f64) -> Vector { - Vector { - x, - y, - z - } - } - - /// Calculates vector magnitude. - pub fn magnitude(&self) -> f64 { - (self.x*self.x + self.y*self.y + self.z*self.z).sqrt() - } - - /// Assigns vector values from another vector. - pub fn assign(&mut self, other: &Vector) { - self.x = other.x; - self.y = other.y; - self.z = other.z; - } - - /// Normalizes vector components to magnitude 1. - pub fn normalize(&mut self) { - let magnitude = self.magnitude(); - self.x /= magnitude; - self.y /= magnitude; - self.z /= magnitude; - } - - /// Add this vector and another and return a new vector. - pub fn add(&self, other: &Vector) -> Vector { - Vector::new(self.x + other.x, self.y + other.y, self.z + other.z) - } - - pub fn dot(&self, other: &Vector) -> f64 { - self.x*other.x + self.y*other.y + self.z*other.z - } -} - -/// Vector4 is a trajectory-tracking object that includes x, y, z, and the current energy. -#[derive(Clone)] -pub struct Vector4 { - pub E: f64, - pub x: f64, - pub y: f64, - pub z: f64, -} - -impl Vector4 { - pub fn new(E: f64, x: f64, y: f64, z: f64) -> Vector4 { - Vector4 { - E, - x, - y, - z - } - } -} - -/// Energy loss is an output tracker that tracks the separate nuclear and electronic energy losses. -#[derive(Clone)] -pub struct EnergyLoss { - pub En: f64, - pub Ee: f64, - pub x: f64, - pub y: f64, - pub z: f64, -} - -impl EnergyLoss { - pub fn new(Ee: f64, En: f64, x: f64, y: f64, z: f64) -> EnergyLoss { - EnergyLoss { - En, - Ee, - x, - y, - z - } - } -} +/// 3D vector. +#[derive(Clone)] +pub struct Vector { + pub x: f64, + pub y: f64, + pub z: f64, +} +impl Vector { + pub fn new(x: f64, y: f64, z: f64) -> Vector { + Vector { + x, + y, + z + } + } + + /// Calculates vector magnitude. + pub fn magnitude(&self) -> f64 { + (self.x*self.x + self.y*self.y + self.z*self.z).sqrt() + } + + /// Assigns vector values from another vector. + pub fn assign(&mut self, other: &Vector) { + self.x = other.x; + self.y = other.y; + self.z = other.z; + } + + /// Normalizes vector components to magnitude 1. + pub fn normalize(&mut self) { + let magnitude = self.magnitude(); + self.x /= magnitude; + self.y /= magnitude; + self.z /= magnitude; + } + + /// Add this vector and another and return a new vector. + pub fn add(&self, other: &Vector) -> Vector { + Vector::new(self.x + other.x, self.y + other.y, self.z + other.z) + } + + pub fn dot(&self, other: &Vector) -> f64 { + self.x*other.x + self.y*other.y + self.z*other.z + } +} + +/// Vector4 is a trajectory-tracking object that includes x, y, z, and the current energy. +#[derive(Clone)] +pub struct Vector4 { + pub E: f64, + pub x: f64, + pub y: f64, + pub z: f64, +} + +impl Vector4 { + pub fn new(E: f64, x: f64, y: f64, z: f64) -> Vector4 { + Vector4 { + E, + x, + y, + z + } + } +} + +/// Energy loss is an output tracker that tracks the separate nuclear and electronic energy losses. +#[derive(Clone)] +pub struct EnergyLoss { + pub En: f64, + pub Ee: f64, + pub x: f64, + pub y: f64, + pub z: f64, +} + +impl EnergyLoss { + pub fn new(Ee: f64, En: f64, x: f64, y: f64, z: f64) -> EnergyLoss { + EnergyLoss { + En, + Ee, + x, + y, + z + } + } +} diff --git a/src/tests.rs b/src/tests.rs index 23d41bb..f93667b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,1054 +1,1054 @@ -#[cfg(test)] -use super::*; -#[cfg(test)] -use float_cmp::*; - -#[test] -#[cfg(feature = "parry3d")] -fn test_parry_cuboid() { - let mass = 1.; - let Z = 1.; - let E = 10.*EV; - let Ec = 1.*EV; - let Es = 5.76*EV; - let x = 0.; - let y = 0.; - let z = 0.; - let cosx = 1./(2.0_f64).sqrt(); - let cosy = 1./(2.0_f64).sqrt(); - let cosz = 0.; - let particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - let material_parameters = material::MaterialParameters{ - energy_unit: "EV".to_string(), - mass_unit: "AMU".to_string(), - Eb: vec![0.0, 0.0], - Es: vec![2.0, 4.0], - Ec: vec![1.0, 1.0], - Z: vec![29., 1.], - m: vec![63.54, 1.0008], - interaction_index: vec![0, 0], - surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, - bulk_binding_model: BulkBindingModel::INDIVIDUAL, - }; - - let radius = 1000.*ANGSTROM; - - //cuboid centered at (0, 0, 0) with l=w=h=1 Angstrom - let geometry_input = parry::ParryTriMeshInput { - length_unit: "ANGSTROM".to_string(), - densities: vec![0.03, 0.03], - electronic_stopping_correction_factor: 1.0, - vertices: vec![ - [-0.5, -0.5, 0.5], - [0.5, -0.5, 0.5], - [-0.5, 0.5, 0.5], - [0.5, 0.5, 0.5], - [-0.5, 0.5, -0.5], - [0.5, 0.5, -0.5], - [-0.5, -0.5, -0.5], - [0.5, -0.5, -0.5], - ], - indices: vec![ - [0, 1, 3], - [0, 3, 2], - [2, 3, 5], - [2, 5, 4], - [4, 5, 7], - [4, 7, 6], - [6, 7, 1], - [6, 1, 0], - [1, 7, 5], - [1, 5, 3], - [6, 0, 2], - [6, 2, 4] - ] - }; - - let mut material_cuboid: material::Material = material::Material::::new(&material_parameters, &geometry_input); - material_cuboid.geometry.energy_barrier_thickness = 10.*ANGSTROM; - - let surface_binding_energy = material_cuboid.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - assert!(approx_eq!(f64, surface_binding_energy/EV, (2. + 4.)/2., epsilon=1E-24)); - - //Test origin is inside - assert!(material_cuboid.geometry.inside(0., 0., 0.)); - - //Test all outside areas for containment - assert!(!material_cuboid.geometry.inside(0., 1., 0.)); - assert!(!material_cuboid.geometry.inside(0., -1., 0.)); - assert!(!material_cuboid.geometry.inside(1., 0., 0.)); - assert!(!material_cuboid.geometry.inside(-1., 0., 0.)); - assert!(!material_cuboid.geometry.inside(0., 0., -1.)); - assert!(!material_cuboid.geometry.inside(0., 0., -1.)); - - //distance to origin - let (x, y, z) = material_cuboid.geometry.closest_point(0., 0., 0.); - assert!(approx_eq!(f64, (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(), 0.5*ANGSTROM, epsilon=1E-24)); - - //distance to wall should be zero at wall - let (x, y, z) = material_cuboid.geometry.closest_point(0.5*ANGSTROM, 0., 0.); - assert!(approx_eq!(f64, ((x - 0.5*ANGSTROM).powi(2) + y.powi(2) + z.powi(2)).sqrt(), 0.0, epsilon=1E-24)); -} - -#[test] -#[cfg(feature = "parry3d")] -fn test_parry_sphere() { - let mass = 1.; - let Z = 1.; - let E = 10.*EV; - let Ec = 1.*EV; - let Es = 5.76*EV; - let x = 0.; - let y = 0.; - let z = 0.; - let cosx = 1./(2.0_f64).sqrt(); - let cosy = 1./(2.0_f64).sqrt(); - let cosz = 0.; - let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - let material_parameters = material::MaterialParameters{ - energy_unit: "EV".to_string(), - mass_unit: "AMU".to_string(), - Eb: vec![0.0, 0.0], - Es: vec![2.0, 4.0], - Ec: vec![1.0, 1.0], - Z: vec![29., 1.], - m: vec![63.54, 1.0008], - interaction_index: vec![0, 0], - surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, - bulk_binding_model: BulkBindingModel::INDIVIDUAL, - }; - - let radius = 1000.*ANGSTROM; - - let geometry_input_sphere = parry::ParryBallInput { - length_unit: "ANGSTROM".to_string(), - radius: 1000.0, - densities: vec![0.03, 0.03], - electronic_stopping_correction_factor: 1.0 - }; - - let mut material_sphere: material::Material = material::Material::::new(&material_parameters, &geometry_input_sphere); - material_sphere.geometry.energy_barrier_thickness = 10.*ANGSTROM; - - particle_1.pos.x = 500.*ANGSTROM; - particle_1.pos.y = 0.; - - particle_1.pos_old.x = -1500.*ANGSTROM; - particle_1.pos_old.y = 0.; - - let inside_sphere = material_sphere.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - let inside_old_sphere = material_sphere.inside(particle_1.pos_old.x, particle_1.pos_old.y, particle_1.pos_old.z); - - //println!("{} {}", inside, inside_old); - assert!(inside_sphere); - assert!(!inside_old_sphere); - - //Test concentration-dependent surface binding energy - let surface_binding_energy = material_sphere.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - assert!(approx_eq!(f64, surface_binding_energy/EV, (2. + 4.)/2., epsilon=1E-24)); - - assert!(material_sphere.inside_energy_barrier(0., 0., 0.)); - assert!(material_sphere.inside_simulation_boundary(0., 0., 0.)); - assert!(material_sphere.inside(0., 0., 0.)); - - let ux = 1214.0*ANGSTROM; - let uy = 123123.0*ANGSTROM; - let uz = 1239.0*ANGSTROM; - let r = (ux.powi(2) + uy.powi(2) + uz.powi(2)).sqrt(); - let R = 1001.0*ANGSTROM; - let u = (ux/r*R, uy/r*R, uz/r*R); - assert!(!material_sphere.inside(u.0, u.1, u.2)); - assert!(material_sphere.inside_energy_barrier(u.0, u.1, u.2)); - assert!(material_sphere.inside_simulation_boundary(u.0, u.1, u.2)); - - assert!(material_sphere.inside_energy_barrier(0., 0., 0.)); - assert!(material_sphere.inside_simulation_boundary(0., 0., 0.)); - assert!(material_sphere.inside(0., 0., 0.)); - - assert!(material_sphere.inside_energy_barrier(-1000.0*ANGSTROM - 1.0*ANGSTROM, 0., 0.)); - assert!(!material_sphere.inside_energy_barrier(-1000.0*ANGSTROM - 11.0*ANGSTROM, 0., 0.)); - - assert!(material_sphere.inside_energy_barrier(0., -1000.0*ANGSTROM - 1.0*ANGSTROM, 0.)); - assert!(!material_sphere.inside_energy_barrier(0., -1000.0*ANGSTROM - 11.0*ANGSTROM, 0.)); - - assert!(material_sphere.inside_energy_barrier(0., 0., -1000.0*ANGSTROM - 1.0*ANGSTROM)); - assert!(!material_sphere.inside_energy_barrier(0., 0., -1000.0*ANGSTROM - 11.0*ANGSTROM)); - - assert!(material_sphere.inside(-1000.0*ANGSTROM + 1.0*ANGSTROM, 0., 0.)); - assert!(!material_sphere.inside(-1000.0*ANGSTROM - 1.0*ANGSTROM, 0., 0.)); - - assert!(material_sphere.inside(0., -1000.0*ANGSTROM + 1.0*ANGSTROM, 0.)); - assert!(!material_sphere.inside(0., -1000.0*ANGSTROM - 1.0*ANGSTROM, 0.)); - - assert!(material_sphere.inside(0., 0., -1000.0*ANGSTROM + 1.0*ANGSTROM)); - assert!(!material_sphere.inside(0., 0., -1000.0*ANGSTROM - 1.0*ANGSTROM)); - - assert!(approx_eq!(f64, material_sphere.closest_point(-2000.*ANGSTROM, 0., 0.).0, (-1000.*ANGSTROM, 0., 0.).0, epsilon=1E-12)); - assert!(approx_eq!(f64, material_sphere.closest_point(0., -2000.*ANGSTROM, 0.).1, (0., -1000.*ANGSTROM, 0.).1, epsilon=1E-12)); - assert!(approx_eq!(f64, material_sphere.closest_point(0., 0., -2000.*ANGSTROM).2, (0., 0., -1000.*ANGSTROM).2, epsilon=1E-12)); -} - -#[test] -#[cfg(feature = "distributions")] -fn test_distributions() { - - let options = Options { - name: "test".to_string(), - track_trajectories: false, - track_recoils: false, - track_recoil_trajectories: false, - write_buffer_size: 8000, - weak_collision_order: 0, - suppress_deep_recoils: false, - high_energy_free_flight_paths: false, - electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, - mean_free_path_model: MeanFreePathModel::LIQUID, - interaction_potential: vec![vec![InteractionPotential::KR_C]], - scattering_integral: vec![vec![ScatteringIntegral::MENDENHALL_WELLER]], - num_threads: 1, - num_chunks: 1, - use_hdf5: false, - root_finder: vec![vec![Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-3}]], - track_displacements: false, - track_energy_losses: true, - energy_min: 0.0, - energy_max: 10.0, - energy_num: 11, - angle_min: 0.0, - angle_max: 90.0, - angle_num: 11, - x_min: 0.0, - y_min: -10.0, - z_min: -10.0, - x_max: 10.0, - y_max: 10.0, - z_max: 10.0, - x_num: 11, - y_num: 11, - z_num: 11, - }; - - let output_units = OutputUnits { - length_unit: 1.0, - energy_unit: 1.0, - mass_unit: 1.0, - }; - - let mass = 1.0; - let Z = 1.0; - let E = 1.5; - let Ec = 0.0; - let Es = 0.0; - let x = 0.0; - let y = 0.0; - let z = 0.0; - let cosx = 0.0; - let cosy = 0.0; - let cosz = 0.0; - let mut particle = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - - - let mut distributions = output::Distributions::new(&options); - assert_eq!(distributions.x_range[0], 0.); - assert_eq!(distributions.x_range[distributions.x_range.len() - 1], 10.); - assert_eq!(distributions.y_range[0], -10.); - assert_eq!(distributions.y_range[distributions.y_range.len() - 1], 10.); - assert_eq!(distributions.z_range[0], -10.); - assert_eq!(distributions.z_range[distributions.z_range.len() - 1], 10.); - assert_eq!(distributions.x_range.len(), 11); - assert_eq!(distributions.y_range.len(), 11); - assert_eq!(distributions.z_range.len(), 11); - - particle.incident = true; - particle.stopped = true; - particle.pos.x = 0.0; - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.implanted_x[0], 1); - - particle.pos.x = 10.0; - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.implanted_x[10], 1); - - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.implanted_y[5], 3); - assert_eq!(distributions.implanted_z[5], 3); - - particle.incident = false; - particle.stopped = false; - particle.left = true; - particle.E = 0.0; - particle.dir.x = -0.999; - particle.dir.y = 0.001; - particle.dir.z = 0.0; - particle.dir.normalize(); - - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.sputtered_ead[[0, 0]], 1); - - particle.E = -0.1; - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.sputtered_ead[[0, 0]], 1); - - particle.E = 10.0; - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.sputtered_ead[[10, 0]], 1); - - particle.dir = Vector::new(-0.707, 0.707, 0.0); - particle.dir.normalize(); - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.sputtered_ead[[10, 5]], 1); - - particle.incident = true; - particle.E = 0.0; - particle.dir.x = -0.999; - particle.dir.y = 0.001; - particle.dir.z = 0.0; - particle.dir.normalize(); - - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.reflected_ead[[0, 0]], 1); - - particle.E = -0.1; - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.reflected_ead[[0, 0]], 1); - - particle.E = 10.0; - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.reflected_ead[[10, 0]], 1); - - particle.dir = Vector::new(-0.707, 0.707, 0.0); - particle.dir.normalize(); - distributions.update(&particle, &output_units, &options, 1); - assert_eq!(distributions.reflected_ead[[10, 5]], 1); -} - -#[test] -fn test_spherical_geometry() { - let mass = 1.; - let Z = 1.; - let E = 10.*EV; - let Ec = 1.*EV; - let Es = 5.76*EV; - let x = 0.; - let y = 0.; - let z = 0.; - let cosx = 1./(2.0_f64).sqrt(); - let cosy = 1./(2.0_f64).sqrt(); - let cosz = 0.; - let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - let material_parameters = material::MaterialParameters{ - energy_unit: "EV".to_string(), - mass_unit: "AMU".to_string(), - Eb: vec![0.0, 0.0], - Es: vec![2.0, 4.0], - Ec: vec![1.0, 1.0], - Z: vec![29., 1.], - m: vec![63.54, 1.0008], - interaction_index: vec![0, 0], - surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, - bulk_binding_model: BulkBindingModel::INDIVIDUAL, - }; - - let radius = 1000.*ANGSTROM; - - let geometry_input_sphere = sphere::SphereInput { - length_unit: "ANGSTROM".to_string(), - radius: 1000.0, - densities: vec![0.03, 0.03], - electronic_stopping_correction_factor: 1.0 - }; - - let mut material_sphere: material::Material = material::Material::::new(&material_parameters, &geometry_input_sphere); - material_sphere.geometry.energy_barrier_thickness = 10.*ANGSTROM; - - particle_1.pos.x = 500.*ANGSTROM; - particle_1.pos.y = 0.; - - particle_1.pos_old.x = -1500.*ANGSTROM; - particle_1.pos_old.y = 0.; - - let inside_sphere = material_sphere.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - let inside_old_sphere = material_sphere.inside(particle_1.pos_old.x, particle_1.pos_old.y, particle_1.pos_old.z); - - //println!("{} {}", inside, inside_old); - assert!(inside_sphere); - assert!(!inside_old_sphere); - - //Test concentration-dependent surface binding energy - let surface_binding_energy = material_sphere.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - assert!(approx_eq!(f64, surface_binding_energy/EV, (2. + 4.)/2., epsilon=1E-24)); - - assert!(material_sphere.inside_energy_barrier(0., 0., 0.)); - assert!(material_sphere.inside_simulation_boundary(0., 0., 0.)); - assert!(material_sphere.inside(0., 0., 0.)); - - let ux = 1214.0*ANGSTROM; - let uy = 123123.0*ANGSTROM; - let uz = 1239.0*ANGSTROM; - let r = (ux.powi(2) + uy.powi(2) + uz.powi(2)).sqrt(); - let R = 1001.0*ANGSTROM; - let u = (ux/r*R, uy/r*R, uz/r*R); - assert!(!material_sphere.inside(u.0, u.1, u.2)); - assert!(material_sphere.inside_energy_barrier(u.0, u.1, u.2)); - assert!(material_sphere.inside_simulation_boundary(u.0, u.1, u.2)); - - assert!(material_sphere.inside_energy_barrier(0., 0., 0.)); - assert!(material_sphere.inside_simulation_boundary(0., 0., 0.)); - assert!(material_sphere.inside(0., 0., 0.)); - - assert!(material_sphere.inside_energy_barrier(-1000.0*ANGSTROM - 1.0*ANGSTROM, 0., 0.)); - assert!(!material_sphere.inside_energy_barrier(-1000.0*ANGSTROM - 11.0*ANGSTROM, 0., 0.)); - - assert!(material_sphere.inside_energy_barrier(0., -1000.0*ANGSTROM - 1.0*ANGSTROM, 0.)); - assert!(!material_sphere.inside_energy_barrier(0., -1000.0*ANGSTROM - 11.0*ANGSTROM, 0.)); - - assert!(material_sphere.inside_energy_barrier(0., 0., -1000.0*ANGSTROM - 1.0*ANGSTROM)); - assert!(!material_sphere.inside_energy_barrier(0., 0., -1000.0*ANGSTROM - 11.0*ANGSTROM)); - - assert!(material_sphere.inside(-1000.0*ANGSTROM + 1.0*ANGSTROM, 0., 0.)); - assert!(!material_sphere.inside(-1000.0*ANGSTROM - 1.0*ANGSTROM, 0., 0.)); - - assert!(material_sphere.inside(0., -1000.0*ANGSTROM + 1.0*ANGSTROM, 0.)); - assert!(!material_sphere.inside(0., -1000.0*ANGSTROM - 1.0*ANGSTROM, 0.)); - - assert!(material_sphere.inside(0., 0., -1000.0*ANGSTROM + 1.0*ANGSTROM)); - assert!(!material_sphere.inside(0., 0., -1000.0*ANGSTROM - 1.0*ANGSTROM)); - - assert!(approx_eq!(f64, material_sphere.closest_point(-2000.*ANGSTROM, 0., 0.).0, (-1000.*ANGSTROM, 0., 0.).0, epsilon=1E-12)); - assert!(approx_eq!(f64, material_sphere.closest_point(0., -2000.*ANGSTROM, 0.).1, (0., -1000.*ANGSTROM, 0.).1, epsilon=1E-12)); - assert!(approx_eq!(f64, material_sphere.closest_point(0., 0., -2000.*ANGSTROM).2, (0., 0., -1000.*ANGSTROM).2, epsilon=1E-12)); - -} - -#[test] -fn test_geometry() { - let mass = 1.; - let Z = 1.; - let E = 10.*EV; - let Ec = 1.*EV; - let Es = 5.76*EV; - let x = 0.; - let y = 0.; - let z = 0.; - let cosx = 1./(2.0_f64).sqrt(); - let cosy = 1./(2.0_f64).sqrt(); - let cosz = 0.; - let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - let material_parameters = material::MaterialParameters{ - energy_unit: "EV".to_string(), - mass_unit: "AMU".to_string(), - Eb: vec![0.0, 0.0], - Es: vec![2.0, 4.0], - Ec: vec![1.0, 1.0], - Z: vec![29., 1.], - m: vec![63.54, 1.0008], - interaction_index: vec![0, 0], - surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, - bulk_binding_model: BulkBindingModel::INDIVIDUAL, - }; - - let thickness: f64 = 1000.; - let depth: f64 = 1000.; - - let geometry_input_2D = geometry::Mesh2DInput { - length_unit: "ANGSTROM".to_string(), - triangles: vec![(0., depth, 0., thickness/2., thickness/2., -thickness/2.), (depth, depth, 0., thickness/2., -thickness/2., -thickness/2.)], - densities: vec![vec![0.03, 0.03], vec![0.03, 0.03]], - material_boundary_points: vec![(0., thickness/2.), (depth, thickness/2.), (depth, -thickness/2.), (0., -thickness/2.), (0., thickness/2.)], - simulation_boundary_points: vec![(0., 1.1*thickness/2.), (depth, 1.1*thickness/2.), (depth, -1.1*thickness/2.), (0., -1.1*thickness/2.), (0., 1.1*thickness/2.)], - energy_barrier_thickness: 10., - electronic_stopping_correction_factors: vec![1.0, 1.0], - }; - - let geometry_input_0D = geometry::Mesh0DInput { - length_unit: "ANGSTROM".to_string(), - densities: vec![0.03, 0.03], - electronic_stopping_correction_factor: 1.0 - }; - - let material_2D: material::Material = material::Material::::new(&material_parameters, &geometry_input_2D); - let mut material_0D: material::Material = material::Material::::new(&material_parameters, &geometry_input_0D); - material_0D.geometry.energy_barrier_thickness = 10.*ANGSTROM; - - particle_1.pos.x = 500.*ANGSTROM; - particle_1.pos.y = 0.; - - particle_1.pos_old.x = -500.*ANGSTROM; - particle_1.pos_old.y = 0.; - - assert_eq!(material_2D.geometry.get_ck(0., 0., 0.), material_0D.geometry.get_ck(0., 0., 0.)); - assert_eq!(material_2D.geometry.get_densities(0., 0., 0.), material_0D.geometry.get_densities(0., 0., 0.)); - assert_eq!(material_2D.geometry.get_total_density(0., 0., 0.), material_0D.geometry.get_total_density(0., 0., 0.)); - assert_eq!(material_2D.geometry.get_concentrations(0., 0., 0.), material_0D.geometry.get_concentrations(0., 0., 0.)); - assert_eq!(material_2D.geometry.closest_point(-10., 0., 5.), material_0D.geometry.closest_point(-10., 0., 5.)); - assert_eq!(material_2D.geometry.get_densities(-10., 0., 5.), material_0D.geometry.get_densities(-10., 0., 5.)); - assert_eq!(material_2D.geometry.get_ck(-10., 0., 5.), material_0D.geometry.get_ck(-10., 0., 5.)); -} - -#[test] -fn test_surface_binding_energy_barrier() { - let mass = 1.; - let Z = 1.; - let E = 10.*EV; - let Ec = 1.*EV; - let Es = 5.76*EV; - let x = 0.; - let y = 0.; - let z = 0.; - let cosx = 1./(2.0_f64).sqrt(); - let cosy = 1./(2.0_f64).sqrt(); - let cosz = 0.; - let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - let material_parameters = material::MaterialParameters{ - energy_unit: "EV".to_string(), - mass_unit: "AMU".to_string(), - Eb: vec![0.0, 0.0], - Es: vec![2.0, 4.0], - Ec: vec![1.0, 1.0], - Z: vec![29., 1.], - m: vec![63.54, 1.0008], - interaction_index: vec![0, 0], - surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, - bulk_binding_model: BulkBindingModel::INDIVIDUAL, - }; - - let thickness: f64 = 1000.; - let depth: f64 = 1000.; - - let geometry_input_2D = geometry::Mesh2DInput { - length_unit: "ANGSTROM".to_string(), - triangles: vec![(0., depth, 0., thickness/2., thickness/2., -thickness/2.), (depth, depth, 0., thickness/2., -thickness/2., -thickness/2.)], - densities: vec![vec![0.03, 0.03], vec![0.03, 0.03]], - material_boundary_points: vec![(0., thickness/2.), (depth, thickness/2.), (depth, -thickness/2.), (0., -thickness/2.), (0., thickness/2.)], - simulation_boundary_points: vec![(0., 1.1*thickness/2.), (depth, 1.1*thickness/2.), (depth, -1.1*thickness/2.), (0., -1.1*thickness/2.), (0., 1.1*thickness/2.)], - energy_barrier_thickness: 10., - electronic_stopping_correction_factors: vec![1.0, 1.0], - }; - - let geometry_input_0D = geometry::Mesh0DInput { - length_unit: "ANGSTROM".to_string(), - densities: vec![0.03, 0.03], - electronic_stopping_correction_factor: 1.0 - }; - - let material_2D: material::Material = material::Material::::new(&material_parameters, &geometry_input_2D); - let mut material_0D: material::Material = material::Material::::new(&material_parameters, &geometry_input_0D); - material_0D.geometry.energy_barrier_thickness = 10.*ANGSTROM; - - particle_1.pos.x = 500.*ANGSTROM; - particle_1.pos.y = 0.; - - particle_1.pos_old.x = -500.*ANGSTROM; - particle_1.pos_old.y = 0.; - - let inside_0D = material_0D.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - let inside_old_0D = material_0D.inside(particle_1.pos_old.x, particle_1.pos_old.y, particle_1.pos_old.z); - - let inside_2D = material_2D.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - let inside_old_2D = material_2D.inside(particle_1.pos_old.x, particle_1.pos_old.y, particle_1.pos_old.z); - - //println!("{} {}", inside, inside_old); - assert!(inside_0D); - assert!(!inside_old_0D); - - assert!(inside_2D); - assert!(!inside_old_2D); - - //Test concentration-dependent surface binding energy - let surface_binding_energy_2D = material_2D.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - let surface_binding_energy_0D = material_0D.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); - - assert!(approx_eq!(f64, surface_binding_energy_0D/EV, (2. + 4.)/2., epsilon=1E-24)); - assert!(approx_eq!(f64, surface_binding_energy_2D/EV, (2. + 4.)/2., epsilon=1E-24)); - - println!("sbv 0D: {}", surface_binding_energy_0D/EV); - println!("sbv 2D: {}", surface_binding_energy_2D/EV); - - //Test leftmost boundary - assert!(material_2D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); - assert!(material_2D.inside_energy_barrier(-5.*ANGSTROM, 0., 0.)); - assert!(!material_2D.inside_energy_barrier(-15.*ANGSTROM, 0., 0.)); - - assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); - assert!(material_0D.inside_energy_barrier(-5.*ANGSTROM, 0., 0.)); - assert!(!material_0D.inside_energy_barrier(-15.*ANGSTROM, 0., 0.)); - - assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); - assert!(!material_0D.inside_energy_barrier(-500.*ANGSTROM, 0., 0.)); - assert!(material_0D.inside_energy_barrier(-9.*ANGSTROM, 0., 0.)); - assert!(!material_0D.inside_energy_barrier(-11.*ANGSTROM, 0., 0.)); - - //Test top boundary - assert!(material_2D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); - assert!(material_2D.inside_energy_barrier(500.*ANGSTROM, 505.*ANGSTROM, 0.)); - assert!(!material_2D.inside_energy_barrier(500.*ANGSTROM, 515.*ANGSTROM, 0.)); - - assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); - assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 505.*ANGSTROM, 0.)); - assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 515.*ANGSTROM, 0.)); - - //Test bottom boundary - assert!(material_2D.inside_energy_barrier(500.*ANGSTROM, -505.*ANGSTROM, 0.)); - assert!(!material_2D.inside_energy_barrier(500.*ANGSTROM, -515.*ANGSTROM, 0.)); - - assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, -505.*ANGSTROM, 0.)); - assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, -515.*ANGSTROM, 0.)); - - //Test rightmost boundary - assert!(material_2D.inside_energy_barrier(1005.*ANGSTROM, 0., 0.)); - assert!(!material_2D.inside_energy_barrier(1015.*ANGSTROM, 0., 0.)); - - assert!(material_0D.inside_energy_barrier(1005.*ANGSTROM, 0., 0.)); - assert!(material_0D.inside_energy_barrier(1015.*ANGSTROM, 0., 0.)); -} - -#[test] -fn test_triangle_contains() { - let triangle_1 = geometry::Triangle2D::new((0., 2., 0., 2., 0., 0.)); - assert!(triangle_1.contains(0.5, 0.5)); - assert!(!triangle_1.contains(2., 2.)); - - let triangle_2 = geometry::Triangle2D::new((-2., 0., 0., 0., 0., -2.)); - assert!(triangle_2.contains(-0.5, -0.5)); - assert!(!triangle_2.contains(0.5, 0.5)); - assert!(!triangle_2.contains(-2., -2.)); -} - -#[test] -fn test_triangle_distance_to() { - let triangle_1 = geometry::Triangle2D::new((0., 2., 0., 2., 0., 0.)); - assert!(approx_eq!(f64, triangle_1.distance_to(-2., 0.), 2., epsilon=1E-12), "{}", triangle_1.distance_to(-2., 0.)); - - assert!(approx_eq!(f64, triangle_1.distance_to(2., 2.), (2.0_f64).sqrt(), epsilon=1E-12), "{}", triangle_1.distance_to(2., 2.)); - - assert!(approx_eq!(f64, triangle_1.distance_to(0., 0.), 0., epsilon=1E-12), "{}", triangle_1.distance_to(0., 0.)); - assert!(approx_eq!(f64, triangle_1.distance_to(2., 0.), 0., epsilon=1E-12), "{}", triangle_1.distance_to(2., 0.)); - assert!(approx_eq!(f64, triangle_1.distance_to(0., 2.), 0., epsilon=1E-12), "{}", triangle_1.distance_to(0., 2.)); -} - -#[test] -fn test_surface_refraction() { - let print_output = false; - - let mass = 1.; - let Z = 1.; - let E = 10.*EV; - let Ec = 1.*EV; - let Es = 5.76*EV; - let x = 0.; - let y = 0.; - let z = 0.; - let cosx = 1./(2.0_f64).sqrt(); - let cosy = 1./(2.0_f64).sqrt(); - let cosz = 0.; - let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - //Test particle entering material and gaining energy - - //Eckstein's formulation for particle entering surface - let cosx_new = ((E*cosx*cosx + Es)/(E + Es)).sqrt(); - let sinx = (1. - cosx*cosx).sqrt(); - let sinx_new = (1. - cosx_new*cosx_new).sqrt(); - let cosy_new = cosy*sinx_new/sinx; - let cosz_new = cosz*sinx_new/sinx; - let dir_mag = (cosx_new*cosx_new + cosy_new*cosy_new + cosz_new*cosz_new).sqrt(); - let cosx_new = cosx_new/dir_mag; - let cosy_new = cosy_new/dir_mag; - let cosz_new = cosz_new/dir_mag; - - //let delta_theta = particle::refraction_angle(cosx, E, E + Es); - //particle::rotate_particle(&mut particle_1, delta_theta, 0.); - - let normal = Vector::new(1.0, 0.0, 0.0); - particle::surface_refraction(&mut particle_1, normal, Es); - - if print_output { - println!("dir_mag: {}", dir_mag); - println!("{} {} {}", cosx, cosy, cosz); - println!("{} {} {}", cosx_new, cosy_new, cosz_new); - println!("{} {} {}", particle_1.dir.x, particle_1.dir.y, particle_1.dir.z); - println!(); - } - - assert!(approx_eq!(f64, particle_1.dir.x, cosx_new, epsilon=1E-12)); - assert!(approx_eq!(f64, particle_1.dir.y, cosy_new, epsilon=1E-12)); - assert!(approx_eq!(f64, particle_1.dir.z, cosz_new, epsilon=1E-12)); - - //Test particle leaving material and losing energy - - let cosx_new = ((particle_1.E*particle_1.dir.x*particle_1.dir.x - Es)/(particle_1.E - Es)).sqrt(); - let sinx = (1. - particle_1.dir.x*particle_1.dir.x).sqrt(); - let sinx_new = (1. - cosx_new*cosx_new).sqrt(); - let cosy_new = particle_1.dir.y*sinx_new/sinx; - let cosz_new = particle_1.dir.z*sinx_new/sinx; - - if print_output { - println!("{} {} {}", particle_1.dir.x, particle_1.dir.y, particle_1.dir.z); - } - - //let delta_theta = particle::refraction_angle(particle_1.dir.x, particle_1.E, particle_1.E - Es); - //particle::rotate_particle(&mut particle_1, delta_theta, 0.); - - let normal = Vector::new(1.0, 0.0, 0.0); - particle::surface_refraction(&mut particle_1, normal, -Es); - - - if print_output { - println!("{} {} {}", cosx_new, cosy_new, cosz_new); - println!("{} {} {}", particle_1.dir.x, particle_1.dir.y, particle_1.dir.z); - } - - assert!(approx_eq!(f64, particle_1.dir.x, cosx_new, epsilon=1E-9)); - assert!(approx_eq!(f64, particle_1.dir.y, cosy_new, epsilon=1E-9)); - assert!(approx_eq!(f64, particle_1.dir.z, cosz_new, epsilon=1E-9)); - -} - -#[test] -fn test_momentum_conservation() { - - for energy_eV in vec![1., 10., 100., 1000., 1000.] { - //Aluminum - let m1 = 183.8*AMU; - let Z1 = 74.; - let E1 = energy_eV*EV; - let Ec1 = 1.*EV; - let Es1 = 1.*EV; - let x1 = 0.; - let y1 = 0.; - let z1 = 0.; - - //Aluminum - let m2 = 6.941; - let Z2 = 3.; - let Ec2 = 1.; - let Es2 = 1.; - - //Arbitrary initial angle - let theta = 0.974194583091052_f64; - let cosx = (theta).cos(); - let cosy = (theta).sin(); - let cosz = 0.; - - let material_parameters = material::MaterialParameters{ - energy_unit: "EV".to_string(), - mass_unit: "AMU".to_string(), - Eb: vec![0.0], - Es: vec![Es2], - Ec: vec![Ec2], - Z: vec![Z2], - m: vec![m2], - interaction_index: vec![0], - surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, - bulk_binding_model: BulkBindingModel::INDIVIDUAL, - }; - - let thickness: f64 = 1000.; - let depth: f64 = 1000.; - let geometry_input = geometry::Mesh2DInput { - length_unit: "ANGSTROM".to_string(), - triangles: vec![(0., depth, 0., thickness/2., thickness/2., -thickness/2.), (depth, depth, 0., thickness/2., -thickness/2., -thickness/2.)], - densities: vec![vec![0.06306], vec![0.06306]], - material_boundary_points: vec![(0., thickness/2.), (depth, thickness/2.), (depth, -thickness/2.), (0., -thickness/2.), (0., thickness/2.)], - simulation_boundary_points: vec![(0., 1.1*thickness/2.), (depth, 1.1*thickness/2.), (depth, -1.1*thickness/2.), (0., -1.1*thickness/2.), (0., 1.1*thickness/2.)], - electronic_stopping_correction_factors: vec![0.0, 0.0], - energy_barrier_thickness: 0., - }; - - let material_1: material::Material = material::Material::::new(&material_parameters, &geometry_input); - - for high_energy_free_flight_paths in vec![true, false] { - for potential in vec![InteractionPotential::KR_C, InteractionPotential::MOLIERE, InteractionPotential::ZBL, InteractionPotential::LENZ_JENSEN] { - for scattering_integral in vec![ScatteringIntegral::MENDENHALL_WELLER, ScatteringIntegral::GAUSS_MEHLER{n_points: 10}, ScatteringIntegral::GAUSS_LEGENDRE] { - for root_finder in vec![Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-3}] { - - println!("Case: {} {} {} {}", energy_eV, high_energy_free_flight_paths, potential, scattering_integral); - - let mut particle_1 = particle::Particle::new(m1, Z1, E1, Ec1, Es1, x1, y1, z1, cosx, cosy, cosz, false, false, 0); - - #[cfg(not(feature = "distributions"))] - let options = Options { - name: "test".to_string(), - track_trajectories: false, - track_recoils: true, - track_recoil_trajectories: false, - write_buffer_size: 8000, - weak_collision_order: 0, - suppress_deep_recoils: false, - high_energy_free_flight_paths: high_energy_free_flight_paths, - electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, - mean_free_path_model: MeanFreePathModel::LIQUID, - interaction_potential: vec![vec![potential]], - scattering_integral: vec![vec![scattering_integral]], - num_threads: 1, - num_chunks: 1, - use_hdf5: false, - root_finder: vec![vec![root_finder]], - track_displacements: false, - track_energy_losses: false, - }; - - #[cfg(feature = "distributions")] - let options = Options { - name: "test".to_string(), - track_trajectories: false, - track_recoils: true, - track_recoil_trajectories: false, - write_buffer_size: 8000, - weak_collision_order: 0, - suppress_deep_recoils: false, - high_energy_free_flight_paths: high_energy_free_flight_paths, - electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, - mean_free_path_model: MeanFreePathModel::LIQUID, - interaction_potential: vec![vec![potential]], - scattering_integral: vec![vec![scattering_integral]], - num_threads: 1, - num_chunks: 1, - use_hdf5: false, - root_finder: vec![vec![root_finder]], - track_displacements: false, - track_energy_losses: false, - energy_min: 0.0, - energy_max: 10.0, - energy_num: 11, - angle_min: 0.0, - angle_max: 90.0, - angle_num: 11, - x_min: 0.0, - y_min: -10.0, - z_min: -10.0, - x_max: 10.0, - y_max: 10.0, - z_max: 10.0, - x_num: 11, - y_num: 11, - z_num: 11, - }; - - let binary_collision_geometries = bca::determine_mfp_phi_impact_parameter(&mut particle_1, &material_1, &options); - - println!("Phi: {} rad p: {} Angstrom mfp: {} Angstrom", binary_collision_geometries[0].phi_azimuthal, - binary_collision_geometries[0].impact_parameter/ANGSTROM, - binary_collision_geometries[0].mfp/ANGSTROM); - - let (species_index, mut particle_2) = bca::choose_collision_partner(&mut particle_1, &material_1, - &binary_collision_geometries[0], &options); - - let mom1_0 = particle_1.get_momentum(); - let mom2_0 = particle_2.get_momentum(); - - let initial_momentum = mom1_0.add(&mom2_0); - - let binary_collision_result = bca::calculate_binary_collision(&particle_1, - &particle_2, &binary_collision_geometries[0], &options).unwrap(); - - println!("E_recoil: {} eV Psi: {} rad Psi_recoil: {} rad", binary_collision_result.recoil_energy/EV, - binary_collision_result.psi, - binary_collision_result.psi_recoil); - - println!("Initial Energies: {} eV {} eV", particle_1.E/EV, particle_2.E/EV); - - //Energy transfer to recoil - particle_2.E = binary_collision_result.recoil_energy - material_1.average_bulk_binding_energy(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z); - - //Rotate particle 1, 2 by lab frame scattering angles - particle::rotate_particle(&mut particle_1, binary_collision_result.psi, - binary_collision_geometries[0].phi_azimuthal); - - particle::rotate_particle(&mut particle_2, -binary_collision_result.psi_recoil, - binary_collision_geometries[0].phi_azimuthal); - - //Subtract total energy from all simultaneous collisions and electronic stopping - bca::update_particle_energy(&mut particle_1, &material_1, 0., - binary_collision_result.recoil_energy, 0., particle_2.Z, species_index, &options); - - let mom1_1 = particle_1.get_momentum(); - let mom2_1 = particle_2.get_momentum(); - - let final_momentum = mom1_1.add(&mom2_1); - - println!("Final Energies: {} eV {} eV", particle_1.E/EV, particle_2.E/EV); - println!("X: {} {} {}% Error", initial_momentum.x/ANGSTROM/AMU, final_momentum.x/ANGSTROM/AMU, 100.*(final_momentum.x - initial_momentum.x)/initial_momentum.magnitude()); - println!("Y: {} {} {}% Error", initial_momentum.y/ANGSTROM/AMU, final_momentum.y/ANGSTROM/AMU, 100.*(final_momentum.y - initial_momentum.y)/initial_momentum.magnitude()); - println!("Z: {} {} {}% Error", initial_momentum.z/ANGSTROM/AMU, final_momentum.z/ANGSTROM/AMU, 100.*(final_momentum.z - initial_momentum.z)/initial_momentum.magnitude()); - println!(); - - assert!(approx_eq!(f64, initial_momentum.x, final_momentum.x, epsilon = 1E-12)); - assert!(approx_eq!(f64, initial_momentum.y, final_momentum.y, epsilon = 1E-12)); - assert!(approx_eq!(f64, initial_momentum.z, final_momentum.z, epsilon = 1E-12)); - - assert!(!particle_1.E.is_nan()); - assert!(!particle_2.E.is_nan()); - assert!(!initial_momentum.x.is_nan()); - assert!(!initial_momentum.x.is_nan()); - assert!(!initial_momentum.x.is_nan()); - } - } - } - } - } -} - -#[test] -fn test_rotate_particle() { - let mass = 1.; - let Z = 1.; - let E = 1.; - let Ec = 1.; - let Es = 1.; - let x = 0.; - let y = 0.; - let z = 0.; - let cosx = (PI/4.).cos(); - let cosy = (PI/4.).sin(); - let cosz = 0.; - let psi = -PI/4.; - let phi = 0.; - - let mut particle = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - //Check that rotation in 2D works - particle::rotate_particle(&mut particle, psi, phi); - assert!(approx_eq!(f64, particle.dir.x, 0., epsilon = 1E-12), "particle.dir.x: {} Should be ~0.", particle.dir.x); - assert!(approx_eq!(f64, particle.dir.y, 1., epsilon = 1E-12), "particle.dir.y: {} Should be ~1.", particle.dir.y); - - //Check that rotating back by negative psi returns to the previous values - particle::rotate_particle(&mut particle, -psi, phi); - assert!(approx_eq!(f64, particle.dir.x, cosx, epsilon = 1E-12), "particle.dir.x: {} Should be ~{}", particle.dir.x, cosx); - assert!(approx_eq!(f64, particle.dir.y, cosy, epsilon = 1E-12), "particle.dir.y: {} Should be ~{}", particle.dir.y, cosy); - - //Check that azimuthal rotation by 180 degrees works correctly - let phi = PI; - particle::rotate_particle(&mut particle, psi, phi); - assert!(approx_eq!(f64, particle.dir.x, 1., epsilon = 1E-12), "particle.dir.x: {} Should be ~1.", particle.dir.x); - assert!(approx_eq!(f64, particle.dir.y, 0., epsilon = 1E-12), "particle.dir.y: {} Should be ~0.", particle.dir.y); - - //Check that particle direction vector remains normalized following rotations - assert!(approx_eq!(f64, particle.dir.x.powi(2) + particle.dir.y.powi(2) + particle.dir.z.powi(2), 1.), "Particle direction not normalized."); - -} - -#[test] -fn test_particle_advance() { - let mass = 1.; - let Z = 1.; - let E = 1.; - let Ec = 1.; - let Es = 1.; - let x = 0.; - let y = 0.; - let z = 0.; - let cosx = (PI/4.).cos(); - let cosy = (PI/4.).sin(); - let cosz = 0.; - let mfp = 1.; - let asymptotic_deflection = 0.5; - - let mut particle = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); - - let distance_traveled = particle::particle_advance(&mut particle, mfp, asymptotic_deflection); - - assert_eq!(particle.pos.x, (1. - 0.5)*cosx); - assert_eq!(particle.pos.y, (1. - 0.5)*cosy); - assert_eq!(particle.pos.z, 0.); - assert_eq!(distance_traveled, mfp - asymptotic_deflection); -} - -#[test] -fn test_quadrature() { - let Za = 1.; - let Zb = 13.; - let Ma = 1.008; - let Mb = 26.9815385; - let E0 = 10.*EV; - let p = 1.*ANGSTROM; - let a = interactions::screening_length(Za, Zb, InteractionPotential::KR_C); - - #[cfg(not(feature = "distributions"))] - let options = Options { - name: "test".to_string(), - track_trajectories: false, - track_recoils: true, - track_recoil_trajectories: false, - write_buffer_size: 8000, - weak_collision_order: 0, - suppress_deep_recoils: false, - high_energy_free_flight_paths: false, - electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, - mean_free_path_model: MeanFreePathModel::LIQUID, - interaction_potential: vec![vec![InteractionPotential::KR_C]], - scattering_integral: vec![vec![ScatteringIntegral::MENDENHALL_WELLER]], - num_threads: 1, - num_chunks: 1, - use_hdf5: false, - root_finder: vec![vec![Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-14}]], - track_displacements: false, - track_energy_losses: false, - }; - - #[cfg(feature = "distributions")] - let options = Options { - name: "test".to_string(), - track_trajectories: false, - track_recoils: true, - track_recoil_trajectories: false, - write_buffer_size: 8000, - weak_collision_order: 0, - suppress_deep_recoils: false, - high_energy_free_flight_paths: false, - electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, - mean_free_path_model: MeanFreePathModel::LIQUID, - interaction_potential: vec![vec![InteractionPotential::KR_C]], - scattering_integral: vec![vec![ScatteringIntegral::MENDENHALL_WELLER]], - num_threads: 1, - num_chunks: 1, - use_hdf5: false, - root_finder: vec![vec![Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-14}]], - track_displacements: false, - track_energy_losses: false, - energy_min: 0.0, - energy_max: 10.0, - energy_num: 11, - angle_min: 0.0, - angle_max: 90.0, - angle_num: 11, - x_min: 0.0, - y_min: -10.0, - z_min: -10.0, - x_max: 10.0, - y_max: 10.0, - z_max: 10.0, - x_num: 11, - y_num: 11, - z_num: 11, - }; - - let x0_newton = bca::newton_rootfinder(Za, Zb, Ma, Mb, E0, p, InteractionPotential::KR_C, 100, 1E-12).unwrap(); - - //If cpr_rootfinder is enabled, compare Newton to CPR - they should be nearly identical - #[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] - if let Ok(x0_cpr) = bca::cpr_rootfinder(Za, Zb, Ma, Mb, E0, p, InteractionPotential::KR_C, 2, 1000, 1E-13, 1E-16, 1E-18, 1E6, 1E-18, false) { - println!("CPR: {} Newton: {}", x0_cpr, x0_newton); - assert!(approx_eq!(f64, x0_newton, x0_cpr, epsilon=1E-3)); - }; - - //Compute center of mass deflection angle with each algorithm - let theta_gm = bca::gauss_mehler(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C, 10); - let theta_gl = bca::gauss_legendre(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C); - let theta_mw = bca::mendenhall_weller(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C); - let theta_magic = bca::magic(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C); - - //Gauss-Mehler and Gauss-Legendre should be very close to each other - assert!(approx_eq!(f64, theta_gm, theta_gl, epsilon=0.001)); - assert!(approx_eq!(f64, theta_gm, theta_mw, epsilon=0.001)); - assert!(approx_eq!(f64, theta_gm, theta_magic, epsilon=0.15)); - - println!("Gauss-Mehler: {} Gauss-Legendre: {} Mendenhall-Weller: {} MAGIC: {}", - theta_gm, theta_gl, theta_mw, theta_magic); -} +#[cfg(test)] +use super::*; +#[cfg(test)] +use float_cmp::*; + +#[test] +#[cfg(feature = "parry3d")] +fn test_parry_cuboid() { + let mass = 1.; + let Z = 1.; + let E = 10.*EV; + let Ec = 1.*EV; + let Es = 5.76*EV; + let x = 0.; + let y = 0.; + let z = 0.; + let cosx = 1./(2.0_f64).sqrt(); + let cosy = 1./(2.0_f64).sqrt(); + let cosz = 0.; + let particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + let material_parameters = material::MaterialParameters{ + energy_unit: "EV".to_string(), + mass_unit: "AMU".to_string(), + Eb: vec![0.0, 0.0], + Es: vec![2.0, 4.0], + Ec: vec![1.0, 1.0], + Z: vec![29., 1.], + m: vec![63.54, 1.0008], + interaction_index: vec![0, 0], + surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, + bulk_binding_model: BulkBindingModel::INDIVIDUAL, + }; + + let radius = 1000.*ANGSTROM; + + //cuboid centered at (0, 0, 0) with l=w=h=1 Angstrom + let geometry_input = parry::ParryTriMeshInput { + length_unit: "ANGSTROM".to_string(), + densities: vec![0.03, 0.03], + electronic_stopping_correction_factor: 1.0, + vertices: vec![ + [-0.5, -0.5, 0.5], + [0.5, -0.5, 0.5], + [-0.5, 0.5, 0.5], + [0.5, 0.5, 0.5], + [-0.5, 0.5, -0.5], + [0.5, 0.5, -0.5], + [-0.5, -0.5, -0.5], + [0.5, -0.5, -0.5], + ], + indices: vec![ + [0, 1, 3], + [0, 3, 2], + [2, 3, 5], + [2, 5, 4], + [4, 5, 7], + [4, 7, 6], + [6, 7, 1], + [6, 1, 0], + [1, 7, 5], + [1, 5, 3], + [6, 0, 2], + [6, 2, 4] + ] + }; + + let mut material_cuboid: material::Material = material::Material::::new(&material_parameters, &geometry_input); + material_cuboid.geometry.energy_barrier_thickness = 10.*ANGSTROM; + + let surface_binding_energy = material_cuboid.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + assert!(approx_eq!(f64, surface_binding_energy/EV, (2. + 4.)/2., epsilon=1E-24)); + + //Test origin is inside + assert!(material_cuboid.geometry.inside(0., 0., 0.)); + + //Test all outside areas for containment + assert!(!material_cuboid.geometry.inside(0., 1., 0.)); + assert!(!material_cuboid.geometry.inside(0., -1., 0.)); + assert!(!material_cuboid.geometry.inside(1., 0., 0.)); + assert!(!material_cuboid.geometry.inside(-1., 0., 0.)); + assert!(!material_cuboid.geometry.inside(0., 0., -1.)); + assert!(!material_cuboid.geometry.inside(0., 0., -1.)); + + //distance to origin + let (x, y, z) = material_cuboid.geometry.closest_point(0., 0., 0.); + assert!(approx_eq!(f64, (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(), 0.5*ANGSTROM, epsilon=1E-24)); + + //distance to wall should be zero at wall + let (x, y, z) = material_cuboid.geometry.closest_point(0.5*ANGSTROM, 0., 0.); + assert!(approx_eq!(f64, ((x - 0.5*ANGSTROM).powi(2) + y.powi(2) + z.powi(2)).sqrt(), 0.0, epsilon=1E-24)); +} + +#[test] +#[cfg(feature = "parry3d")] +fn test_parry_sphere() { + let mass = 1.; + let Z = 1.; + let E = 10.*EV; + let Ec = 1.*EV; + let Es = 5.76*EV; + let x = 0.; + let y = 0.; + let z = 0.; + let cosx = 1./(2.0_f64).sqrt(); + let cosy = 1./(2.0_f64).sqrt(); + let cosz = 0.; + let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + let material_parameters = material::MaterialParameters{ + energy_unit: "EV".to_string(), + mass_unit: "AMU".to_string(), + Eb: vec![0.0, 0.0], + Es: vec![2.0, 4.0], + Ec: vec![1.0, 1.0], + Z: vec![29., 1.], + m: vec![63.54, 1.0008], + interaction_index: vec![0, 0], + surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, + bulk_binding_model: BulkBindingModel::INDIVIDUAL, + }; + + let radius = 1000.*ANGSTROM; + + let geometry_input_sphere = parry::ParryBallInput { + length_unit: "ANGSTROM".to_string(), + radius: 1000.0, + densities: vec![0.03, 0.03], + electronic_stopping_correction_factor: 1.0 + }; + + let mut material_sphere: material::Material = material::Material::::new(&material_parameters, &geometry_input_sphere); + material_sphere.geometry.energy_barrier_thickness = 10.*ANGSTROM; + + particle_1.pos.x = 500.*ANGSTROM; + particle_1.pos.y = 0.; + + particle_1.pos_old.x = -1500.*ANGSTROM; + particle_1.pos_old.y = 0.; + + let inside_sphere = material_sphere.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + let inside_old_sphere = material_sphere.inside(particle_1.pos_old.x, particle_1.pos_old.y, particle_1.pos_old.z); + + //println!("{} {}", inside, inside_old); + assert!(inside_sphere); + assert!(!inside_old_sphere); + + //Test concentration-dependent surface binding energy + let surface_binding_energy = material_sphere.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + assert!(approx_eq!(f64, surface_binding_energy/EV, (2. + 4.)/2., epsilon=1E-24)); + + assert!(material_sphere.inside_energy_barrier(0., 0., 0.)); + assert!(material_sphere.inside_simulation_boundary(0., 0., 0.)); + assert!(material_sphere.inside(0., 0., 0.)); + + let ux = 1214.0*ANGSTROM; + let uy = 123123.0*ANGSTROM; + let uz = 1239.0*ANGSTROM; + let r = (ux.powi(2) + uy.powi(2) + uz.powi(2)).sqrt(); + let R = 1001.0*ANGSTROM; + let u = (ux/r*R, uy/r*R, uz/r*R); + assert!(!material_sphere.inside(u.0, u.1, u.2)); + assert!(material_sphere.inside_energy_barrier(u.0, u.1, u.2)); + assert!(material_sphere.inside_simulation_boundary(u.0, u.1, u.2)); + + assert!(material_sphere.inside_energy_barrier(0., 0., 0.)); + assert!(material_sphere.inside_simulation_boundary(0., 0., 0.)); + assert!(material_sphere.inside(0., 0., 0.)); + + assert!(material_sphere.inside_energy_barrier(-1000.0*ANGSTROM - 1.0*ANGSTROM, 0., 0.)); + assert!(!material_sphere.inside_energy_barrier(-1000.0*ANGSTROM - 11.0*ANGSTROM, 0., 0.)); + + assert!(material_sphere.inside_energy_barrier(0., -1000.0*ANGSTROM - 1.0*ANGSTROM, 0.)); + assert!(!material_sphere.inside_energy_barrier(0., -1000.0*ANGSTROM - 11.0*ANGSTROM, 0.)); + + assert!(material_sphere.inside_energy_barrier(0., 0., -1000.0*ANGSTROM - 1.0*ANGSTROM)); + assert!(!material_sphere.inside_energy_barrier(0., 0., -1000.0*ANGSTROM - 11.0*ANGSTROM)); + + assert!(material_sphere.inside(-1000.0*ANGSTROM + 1.0*ANGSTROM, 0., 0.)); + assert!(!material_sphere.inside(-1000.0*ANGSTROM - 1.0*ANGSTROM, 0., 0.)); + + assert!(material_sphere.inside(0., -1000.0*ANGSTROM + 1.0*ANGSTROM, 0.)); + assert!(!material_sphere.inside(0., -1000.0*ANGSTROM - 1.0*ANGSTROM, 0.)); + + assert!(material_sphere.inside(0., 0., -1000.0*ANGSTROM + 1.0*ANGSTROM)); + assert!(!material_sphere.inside(0., 0., -1000.0*ANGSTROM - 1.0*ANGSTROM)); + + assert!(approx_eq!(f64, material_sphere.closest_point(-2000.*ANGSTROM, 0., 0.).0, (-1000.*ANGSTROM, 0., 0.).0, epsilon=1E-12)); + assert!(approx_eq!(f64, material_sphere.closest_point(0., -2000.*ANGSTROM, 0.).1, (0., -1000.*ANGSTROM, 0.).1, epsilon=1E-12)); + assert!(approx_eq!(f64, material_sphere.closest_point(0., 0., -2000.*ANGSTROM).2, (0., 0., -1000.*ANGSTROM).2, epsilon=1E-12)); +} + +#[test] +#[cfg(feature = "distributions")] +fn test_distributions() { + + let options = Options { + name: "test".to_string(), + track_trajectories: false, + track_recoils: false, + track_recoil_trajectories: false, + write_buffer_size: 8000, + weak_collision_order: 0, + suppress_deep_recoils: false, + high_energy_free_flight_paths: false, + electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, + mean_free_path_model: MeanFreePathModel::LIQUID, + interaction_potential: vec![vec![InteractionPotential::KR_C]], + scattering_integral: vec![vec![ScatteringIntegral::MENDENHALL_WELLER]], + num_threads: 1, + num_chunks: 1, + use_hdf5: false, + root_finder: vec![vec![Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-3}]], + track_displacements: false, + track_energy_losses: true, + energy_min: 0.0, + energy_max: 10.0, + energy_num: 11, + angle_min: 0.0, + angle_max: 90.0, + angle_num: 11, + x_min: 0.0, + y_min: -10.0, + z_min: -10.0, + x_max: 10.0, + y_max: 10.0, + z_max: 10.0, + x_num: 11, + y_num: 11, + z_num: 11, + }; + + let output_units = OutputUnits { + length_unit: 1.0, + energy_unit: 1.0, + mass_unit: 1.0, + }; + + let mass = 1.0; + let Z = 1.0; + let E = 1.5; + let Ec = 0.0; + let Es = 0.0; + let x = 0.0; + let y = 0.0; + let z = 0.0; + let cosx = 0.0; + let cosy = 0.0; + let cosz = 0.0; + let mut particle = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + + + let mut distributions = output::Distributions::new(&options); + assert_eq!(distributions.x_range[0], 0.); + assert_eq!(distributions.x_range[distributions.x_range.len() - 1], 10.); + assert_eq!(distributions.y_range[0], -10.); + assert_eq!(distributions.y_range[distributions.y_range.len() - 1], 10.); + assert_eq!(distributions.z_range[0], -10.); + assert_eq!(distributions.z_range[distributions.z_range.len() - 1], 10.); + assert_eq!(distributions.x_range.len(), 11); + assert_eq!(distributions.y_range.len(), 11); + assert_eq!(distributions.z_range.len(), 11); + + particle.incident = true; + particle.stopped = true; + particle.pos.x = 0.0; + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.implanted_x[0], 1); + + particle.pos.x = 10.0; + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.implanted_x[10], 1); + + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.implanted_y[5], 3); + assert_eq!(distributions.implanted_z[5], 3); + + particle.incident = false; + particle.stopped = false; + particle.left = true; + particle.E = 0.0; + particle.dir.x = -0.999; + particle.dir.y = 0.001; + particle.dir.z = 0.0; + particle.dir.normalize(); + + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.sputtered_ead[[0, 0]], 1); + + particle.E = -0.1; + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.sputtered_ead[[0, 0]], 1); + + particle.E = 10.0; + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.sputtered_ead[[10, 0]], 1); + + particle.dir = Vector::new(-0.707, 0.707, 0.0); + particle.dir.normalize(); + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.sputtered_ead[[10, 5]], 1); + + particle.incident = true; + particle.E = 0.0; + particle.dir.x = -0.999; + particle.dir.y = 0.001; + particle.dir.z = 0.0; + particle.dir.normalize(); + + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.reflected_ead[[0, 0]], 1); + + particle.E = -0.1; + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.reflected_ead[[0, 0]], 1); + + particle.E = 10.0; + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.reflected_ead[[10, 0]], 1); + + particle.dir = Vector::new(-0.707, 0.707, 0.0); + particle.dir.normalize(); + distributions.update(&particle, &output_units, &options, 1); + assert_eq!(distributions.reflected_ead[[10, 5]], 1); +} + +#[test] +fn test_spherical_geometry() { + let mass = 1.; + let Z = 1.; + let E = 10.*EV; + let Ec = 1.*EV; + let Es = 5.76*EV; + let x = 0.; + let y = 0.; + let z = 0.; + let cosx = 1./(2.0_f64).sqrt(); + let cosy = 1./(2.0_f64).sqrt(); + let cosz = 0.; + let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + let material_parameters = material::MaterialParameters{ + energy_unit: "EV".to_string(), + mass_unit: "AMU".to_string(), + Eb: vec![0.0, 0.0], + Es: vec![2.0, 4.0], + Ec: vec![1.0, 1.0], + Z: vec![29., 1.], + m: vec![63.54, 1.0008], + interaction_index: vec![0, 0], + surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, + bulk_binding_model: BulkBindingModel::INDIVIDUAL, + }; + + let radius = 1000.*ANGSTROM; + + let geometry_input_sphere = sphere::SphereInput { + length_unit: "ANGSTROM".to_string(), + radius: 1000.0, + densities: vec![0.03, 0.03], + electronic_stopping_correction_factor: 1.0 + }; + + let mut material_sphere: material::Material = material::Material::::new(&material_parameters, &geometry_input_sphere); + material_sphere.geometry.energy_barrier_thickness = 10.*ANGSTROM; + + particle_1.pos.x = 500.*ANGSTROM; + particle_1.pos.y = 0.; + + particle_1.pos_old.x = -1500.*ANGSTROM; + particle_1.pos_old.y = 0.; + + let inside_sphere = material_sphere.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + let inside_old_sphere = material_sphere.inside(particle_1.pos_old.x, particle_1.pos_old.y, particle_1.pos_old.z); + + //println!("{} {}", inside, inside_old); + assert!(inside_sphere); + assert!(!inside_old_sphere); + + //Test concentration-dependent surface binding energy + let surface_binding_energy = material_sphere.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + assert!(approx_eq!(f64, surface_binding_energy/EV, (2. + 4.)/2., epsilon=1E-24)); + + assert!(material_sphere.inside_energy_barrier(0., 0., 0.)); + assert!(material_sphere.inside_simulation_boundary(0., 0., 0.)); + assert!(material_sphere.inside(0., 0., 0.)); + + let ux = 1214.0*ANGSTROM; + let uy = 123123.0*ANGSTROM; + let uz = 1239.0*ANGSTROM; + let r = (ux.powi(2) + uy.powi(2) + uz.powi(2)).sqrt(); + let R = 1001.0*ANGSTROM; + let u = (ux/r*R, uy/r*R, uz/r*R); + assert!(!material_sphere.inside(u.0, u.1, u.2)); + assert!(material_sphere.inside_energy_barrier(u.0, u.1, u.2)); + assert!(material_sphere.inside_simulation_boundary(u.0, u.1, u.2)); + + assert!(material_sphere.inside_energy_barrier(0., 0., 0.)); + assert!(material_sphere.inside_simulation_boundary(0., 0., 0.)); + assert!(material_sphere.inside(0., 0., 0.)); + + assert!(material_sphere.inside_energy_barrier(-1000.0*ANGSTROM - 1.0*ANGSTROM, 0., 0.)); + assert!(!material_sphere.inside_energy_barrier(-1000.0*ANGSTROM - 11.0*ANGSTROM, 0., 0.)); + + assert!(material_sphere.inside_energy_barrier(0., -1000.0*ANGSTROM - 1.0*ANGSTROM, 0.)); + assert!(!material_sphere.inside_energy_barrier(0., -1000.0*ANGSTROM - 11.0*ANGSTROM, 0.)); + + assert!(material_sphere.inside_energy_barrier(0., 0., -1000.0*ANGSTROM - 1.0*ANGSTROM)); + assert!(!material_sphere.inside_energy_barrier(0., 0., -1000.0*ANGSTROM - 11.0*ANGSTROM)); + + assert!(material_sphere.inside(-1000.0*ANGSTROM + 1.0*ANGSTROM, 0., 0.)); + assert!(!material_sphere.inside(-1000.0*ANGSTROM - 1.0*ANGSTROM, 0., 0.)); + + assert!(material_sphere.inside(0., -1000.0*ANGSTROM + 1.0*ANGSTROM, 0.)); + assert!(!material_sphere.inside(0., -1000.0*ANGSTROM - 1.0*ANGSTROM, 0.)); + + assert!(material_sphere.inside(0., 0., -1000.0*ANGSTROM + 1.0*ANGSTROM)); + assert!(!material_sphere.inside(0., 0., -1000.0*ANGSTROM - 1.0*ANGSTROM)); + + assert!(approx_eq!(f64, material_sphere.closest_point(-2000.*ANGSTROM, 0., 0.).0, (-1000.*ANGSTROM, 0., 0.).0, epsilon=1E-12)); + assert!(approx_eq!(f64, material_sphere.closest_point(0., -2000.*ANGSTROM, 0.).1, (0., -1000.*ANGSTROM, 0.).1, epsilon=1E-12)); + assert!(approx_eq!(f64, material_sphere.closest_point(0., 0., -2000.*ANGSTROM).2, (0., 0., -1000.*ANGSTROM).2, epsilon=1E-12)); + +} + +#[test] +fn test_geometry() { + let mass = 1.; + let Z = 1.; + let E = 10.*EV; + let Ec = 1.*EV; + let Es = 5.76*EV; + let x = 0.; + let y = 0.; + let z = 0.; + let cosx = 1./(2.0_f64).sqrt(); + let cosy = 1./(2.0_f64).sqrt(); + let cosz = 0.; + let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + let material_parameters = material::MaterialParameters{ + energy_unit: "EV".to_string(), + mass_unit: "AMU".to_string(), + Eb: vec![0.0, 0.0], + Es: vec![2.0, 4.0], + Ec: vec![1.0, 1.0], + Z: vec![29., 1.], + m: vec![63.54, 1.0008], + interaction_index: vec![0, 0], + surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, + bulk_binding_model: BulkBindingModel::INDIVIDUAL, + }; + + let thickness: f64 = 1000.; + let depth: f64 = 1000.; + + let geometry_input_2D = geometry::Mesh2DInput { + length_unit: "ANGSTROM".to_string(), + triangles: vec![(0., depth, 0., thickness/2., thickness/2., -thickness/2.), (depth, depth, 0., thickness/2., -thickness/2., -thickness/2.)], + densities: vec![vec![0.03, 0.03], vec![0.03, 0.03]], + material_boundary_points: vec![(0., thickness/2.), (depth, thickness/2.), (depth, -thickness/2.), (0., -thickness/2.), (0., thickness/2.)], + simulation_boundary_points: vec![(0., 1.1*thickness/2.), (depth, 1.1*thickness/2.), (depth, -1.1*thickness/2.), (0., -1.1*thickness/2.), (0., 1.1*thickness/2.)], + energy_barrier_thickness: 10., + electronic_stopping_correction_factors: vec![1.0, 1.0], + }; + + let geometry_input_0D = geometry::Mesh0DInput { + length_unit: "ANGSTROM".to_string(), + densities: vec![0.03, 0.03], + electronic_stopping_correction_factor: 1.0 + }; + + let material_2D: material::Material = material::Material::::new(&material_parameters, &geometry_input_2D); + let mut material_0D: material::Material = material::Material::::new(&material_parameters, &geometry_input_0D); + material_0D.geometry.energy_barrier_thickness = 10.*ANGSTROM; + + particle_1.pos.x = 500.*ANGSTROM; + particle_1.pos.y = 0.; + + particle_1.pos_old.x = -500.*ANGSTROM; + particle_1.pos_old.y = 0.; + + assert_eq!(material_2D.geometry.get_ck(0., 0., 0.), material_0D.geometry.get_ck(0., 0., 0.)); + assert_eq!(material_2D.geometry.get_densities(0., 0., 0.), material_0D.geometry.get_densities(0., 0., 0.)); + assert_eq!(material_2D.geometry.get_total_density(0., 0., 0.), material_0D.geometry.get_total_density(0., 0., 0.)); + assert_eq!(material_2D.geometry.get_concentrations(0., 0., 0.), material_0D.geometry.get_concentrations(0., 0., 0.)); + assert_eq!(material_2D.geometry.closest_point(-10., 0., 5.), material_0D.geometry.closest_point(-10., 0., 5.)); + assert_eq!(material_2D.geometry.get_densities(-10., 0., 5.), material_0D.geometry.get_densities(-10., 0., 5.)); + assert_eq!(material_2D.geometry.get_ck(-10., 0., 5.), material_0D.geometry.get_ck(-10., 0., 5.)); +} + +#[test] +fn test_surface_binding_energy_barrier() { + let mass = 1.; + let Z = 1.; + let E = 10.*EV; + let Ec = 1.*EV; + let Es = 5.76*EV; + let x = 0.; + let y = 0.; + let z = 0.; + let cosx = 1./(2.0_f64).sqrt(); + let cosy = 1./(2.0_f64).sqrt(); + let cosz = 0.; + let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + let material_parameters = material::MaterialParameters{ + energy_unit: "EV".to_string(), + mass_unit: "AMU".to_string(), + Eb: vec![0.0, 0.0], + Es: vec![2.0, 4.0], + Ec: vec![1.0, 1.0], + Z: vec![29., 1.], + m: vec![63.54, 1.0008], + interaction_index: vec![0, 0], + surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, + bulk_binding_model: BulkBindingModel::INDIVIDUAL, + }; + + let thickness: f64 = 1000.; + let depth: f64 = 1000.; + + let geometry_input_2D = geometry::Mesh2DInput { + length_unit: "ANGSTROM".to_string(), + triangles: vec![(0., depth, 0., thickness/2., thickness/2., -thickness/2.), (depth, depth, 0., thickness/2., -thickness/2., -thickness/2.)], + densities: vec![vec![0.03, 0.03], vec![0.03, 0.03]], + material_boundary_points: vec![(0., thickness/2.), (depth, thickness/2.), (depth, -thickness/2.), (0., -thickness/2.), (0., thickness/2.)], + simulation_boundary_points: vec![(0., 1.1*thickness/2.), (depth, 1.1*thickness/2.), (depth, -1.1*thickness/2.), (0., -1.1*thickness/2.), (0., 1.1*thickness/2.)], + energy_barrier_thickness: 10., + electronic_stopping_correction_factors: vec![1.0, 1.0], + }; + + let geometry_input_0D = geometry::Mesh0DInput { + length_unit: "ANGSTROM".to_string(), + densities: vec![0.03, 0.03], + electronic_stopping_correction_factor: 1.0 + }; + + let material_2D: material::Material = material::Material::::new(&material_parameters, &geometry_input_2D); + let mut material_0D: material::Material = material::Material::::new(&material_parameters, &geometry_input_0D); + material_0D.geometry.energy_barrier_thickness = 10.*ANGSTROM; + + particle_1.pos.x = 500.*ANGSTROM; + particle_1.pos.y = 0.; + + particle_1.pos_old.x = -500.*ANGSTROM; + particle_1.pos_old.y = 0.; + + let inside_0D = material_0D.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + let inside_old_0D = material_0D.inside(particle_1.pos_old.x, particle_1.pos_old.y, particle_1.pos_old.z); + + let inside_2D = material_2D.inside(particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + let inside_old_2D = material_2D.inside(particle_1.pos_old.x, particle_1.pos_old.y, particle_1.pos_old.z); + + //println!("{} {}", inside, inside_old); + assert!(inside_0D); + assert!(!inside_old_0D); + + assert!(inside_2D); + assert!(!inside_old_2D); + + //Test concentration-dependent surface binding energy + let surface_binding_energy_2D = material_2D.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + let surface_binding_energy_0D = material_0D.actual_surface_binding_energy(&particle_1, particle_1.pos.x, particle_1.pos.y, particle_1.pos.z); + + assert!(approx_eq!(f64, surface_binding_energy_0D/EV, (2. + 4.)/2., epsilon=1E-24)); + assert!(approx_eq!(f64, surface_binding_energy_2D/EV, (2. + 4.)/2., epsilon=1E-24)); + + println!("sbv 0D: {}", surface_binding_energy_0D/EV); + println!("sbv 2D: {}", surface_binding_energy_2D/EV); + + //Test leftmost boundary + assert!(material_2D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); + assert!(material_2D.inside_energy_barrier(-5.*ANGSTROM, 0., 0.)); + assert!(!material_2D.inside_energy_barrier(-15.*ANGSTROM, 0., 0.)); + + assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); + assert!(material_0D.inside_energy_barrier(-5.*ANGSTROM, 0., 0.)); + assert!(!material_0D.inside_energy_barrier(-15.*ANGSTROM, 0., 0.)); + + assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); + assert!(!material_0D.inside_energy_barrier(-500.*ANGSTROM, 0., 0.)); + assert!(material_0D.inside_energy_barrier(-9.*ANGSTROM, 0., 0.)); + assert!(!material_0D.inside_energy_barrier(-11.*ANGSTROM, 0., 0.)); + + //Test top boundary + assert!(material_2D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); + assert!(material_2D.inside_energy_barrier(500.*ANGSTROM, 505.*ANGSTROM, 0.)); + assert!(!material_2D.inside_energy_barrier(500.*ANGSTROM, 515.*ANGSTROM, 0.)); + + assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 0., 0.)); + assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 505.*ANGSTROM, 0.)); + assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, 515.*ANGSTROM, 0.)); + + //Test bottom boundary + assert!(material_2D.inside_energy_barrier(500.*ANGSTROM, -505.*ANGSTROM, 0.)); + assert!(!material_2D.inside_energy_barrier(500.*ANGSTROM, -515.*ANGSTROM, 0.)); + + assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, -505.*ANGSTROM, 0.)); + assert!(material_0D.inside_energy_barrier(500.*ANGSTROM, -515.*ANGSTROM, 0.)); + + //Test rightmost boundary + assert!(material_2D.inside_energy_barrier(1005.*ANGSTROM, 0., 0.)); + assert!(!material_2D.inside_energy_barrier(1015.*ANGSTROM, 0., 0.)); + + assert!(material_0D.inside_energy_barrier(1005.*ANGSTROM, 0., 0.)); + assert!(material_0D.inside_energy_barrier(1015.*ANGSTROM, 0., 0.)); +} + +#[test] +fn test_triangle_contains() { + let triangle_1 = geometry::Triangle2D::new((0., 2., 0., 2., 0., 0.)); + assert!(triangle_1.contains(0.5, 0.5)); + assert!(!triangle_1.contains(2., 2.)); + + let triangle_2 = geometry::Triangle2D::new((-2., 0., 0., 0., 0., -2.)); + assert!(triangle_2.contains(-0.5, -0.5)); + assert!(!triangle_2.contains(0.5, 0.5)); + assert!(!triangle_2.contains(-2., -2.)); +} + +#[test] +fn test_triangle_distance_to() { + let triangle_1 = geometry::Triangle2D::new((0., 2., 0., 2., 0., 0.)); + assert!(approx_eq!(f64, triangle_1.distance_to(-2., 0.), 2., epsilon=1E-12), "{}", triangle_1.distance_to(-2., 0.)); + + assert!(approx_eq!(f64, triangle_1.distance_to(2., 2.), (2.0_f64).sqrt(), epsilon=1E-12), "{}", triangle_1.distance_to(2., 2.)); + + assert!(approx_eq!(f64, triangle_1.distance_to(0., 0.), 0., epsilon=1E-12), "{}", triangle_1.distance_to(0., 0.)); + assert!(approx_eq!(f64, triangle_1.distance_to(2., 0.), 0., epsilon=1E-12), "{}", triangle_1.distance_to(2., 0.)); + assert!(approx_eq!(f64, triangle_1.distance_to(0., 2.), 0., epsilon=1E-12), "{}", triangle_1.distance_to(0., 2.)); +} + +#[test] +fn test_surface_refraction() { + let print_output = false; + + let mass = 1.; + let Z = 1.; + let E = 10.*EV; + let Ec = 1.*EV; + let Es = 5.76*EV; + let x = 0.; + let y = 0.; + let z = 0.; + let cosx = 1./(2.0_f64).sqrt(); + let cosy = 1./(2.0_f64).sqrt(); + let cosz = 0.; + let mut particle_1 = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + //Test particle entering material and gaining energy + + //Eckstein's formulation for particle entering surface + let cosx_new = ((E*cosx*cosx + Es)/(E + Es)).sqrt(); + let sinx = (1. - cosx*cosx).sqrt(); + let sinx_new = (1. - cosx_new*cosx_new).sqrt(); + let cosy_new = cosy*sinx_new/sinx; + let cosz_new = cosz*sinx_new/sinx; + let dir_mag = (cosx_new*cosx_new + cosy_new*cosy_new + cosz_new*cosz_new).sqrt(); + let cosx_new = cosx_new/dir_mag; + let cosy_new = cosy_new/dir_mag; + let cosz_new = cosz_new/dir_mag; + + //let delta_theta = particle::refraction_angle(cosx, E, E + Es); + //particle::rotate_particle(&mut particle_1, delta_theta, 0.); + + let normal = Vector::new(1.0, 0.0, 0.0); + particle::surface_refraction(&mut particle_1, normal, Es); + + if print_output { + println!("dir_mag: {}", dir_mag); + println!("{} {} {}", cosx, cosy, cosz); + println!("{} {} {}", cosx_new, cosy_new, cosz_new); + println!("{} {} {}", particle_1.dir.x, particle_1.dir.y, particle_1.dir.z); + println!(); + } + + assert!(approx_eq!(f64, particle_1.dir.x, cosx_new, epsilon=1E-12)); + assert!(approx_eq!(f64, particle_1.dir.y, cosy_new, epsilon=1E-12)); + assert!(approx_eq!(f64, particle_1.dir.z, cosz_new, epsilon=1E-12)); + + //Test particle leaving material and losing energy + + let cosx_new = ((particle_1.E*particle_1.dir.x*particle_1.dir.x - Es)/(particle_1.E - Es)).sqrt(); + let sinx = (1. - particle_1.dir.x*particle_1.dir.x).sqrt(); + let sinx_new = (1. - cosx_new*cosx_new).sqrt(); + let cosy_new = particle_1.dir.y*sinx_new/sinx; + let cosz_new = particle_1.dir.z*sinx_new/sinx; + + if print_output { + println!("{} {} {}", particle_1.dir.x, particle_1.dir.y, particle_1.dir.z); + } + + //let delta_theta = particle::refraction_angle(particle_1.dir.x, particle_1.E, particle_1.E - Es); + //particle::rotate_particle(&mut particle_1, delta_theta, 0.); + + let normal = Vector::new(1.0, 0.0, 0.0); + particle::surface_refraction(&mut particle_1, normal, -Es); + + + if print_output { + println!("{} {} {}", cosx_new, cosy_new, cosz_new); + println!("{} {} {}", particle_1.dir.x, particle_1.dir.y, particle_1.dir.z); + } + + assert!(approx_eq!(f64, particle_1.dir.x, cosx_new, epsilon=1E-9)); + assert!(approx_eq!(f64, particle_1.dir.y, cosy_new, epsilon=1E-9)); + assert!(approx_eq!(f64, particle_1.dir.z, cosz_new, epsilon=1E-9)); + +} + +#[test] +fn test_momentum_conservation() { + + for energy_eV in vec![1., 10., 100., 1000., 1000.] { + //Aluminum + let m1 = 183.8*AMU; + let Z1 = 74.; + let E1 = energy_eV*EV; + let Ec1 = 1.*EV; + let Es1 = 1.*EV; + let x1 = 0.; + let y1 = 0.; + let z1 = 0.; + + //Aluminum + let m2 = 6.941; + let Z2 = 3.; + let Ec2 = 1.; + let Es2 = 1.; + + //Arbitrary initial angle + let theta = 0.974194583091052_f64; + let cosx = (theta).cos(); + let cosy = (theta).sin(); + let cosz = 0.; + + let material_parameters = material::MaterialParameters{ + energy_unit: "EV".to_string(), + mass_unit: "AMU".to_string(), + Eb: vec![0.0], + Es: vec![Es2], + Ec: vec![Ec2], + Z: vec![Z2], + m: vec![m2], + interaction_index: vec![0], + surface_binding_model: SurfaceBindingModel::PLANAR{calculation: SurfaceBindingCalculation::TARGET}, + bulk_binding_model: BulkBindingModel::INDIVIDUAL, + }; + + let thickness: f64 = 1000.; + let depth: f64 = 1000.; + let geometry_input = geometry::Mesh2DInput { + length_unit: "ANGSTROM".to_string(), + triangles: vec![(0., depth, 0., thickness/2., thickness/2., -thickness/2.), (depth, depth, 0., thickness/2., -thickness/2., -thickness/2.)], + densities: vec![vec![0.06306], vec![0.06306]], + material_boundary_points: vec![(0., thickness/2.), (depth, thickness/2.), (depth, -thickness/2.), (0., -thickness/2.), (0., thickness/2.)], + simulation_boundary_points: vec![(0., 1.1*thickness/2.), (depth, 1.1*thickness/2.), (depth, -1.1*thickness/2.), (0., -1.1*thickness/2.), (0., 1.1*thickness/2.)], + electronic_stopping_correction_factors: vec![0.0, 0.0], + energy_barrier_thickness: 0., + }; + + let material_1: material::Material = material::Material::::new(&material_parameters, &geometry_input); + + for high_energy_free_flight_paths in vec![true, false] { + for potential in vec![InteractionPotential::KR_C, InteractionPotential::MOLIERE, InteractionPotential::ZBL, InteractionPotential::LENZ_JENSEN] { + for scattering_integral in vec![ScatteringIntegral::MENDENHALL_WELLER, ScatteringIntegral::GAUSS_MEHLER{n_points: 10}, ScatteringIntegral::GAUSS_LEGENDRE] { + for root_finder in vec![Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-3}] { + + println!("Case: {} {} {} {}", energy_eV, high_energy_free_flight_paths, potential, scattering_integral); + + let mut particle_1 = particle::Particle::new(m1, Z1, E1, Ec1, Es1, x1, y1, z1, cosx, cosy, cosz, false, false, 0); + + #[cfg(not(feature = "distributions"))] + let options = Options { + name: "test".to_string(), + track_trajectories: false, + track_recoils: true, + track_recoil_trajectories: false, + write_buffer_size: 8000, + weak_collision_order: 0, + suppress_deep_recoils: false, + high_energy_free_flight_paths: high_energy_free_flight_paths, + electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, + mean_free_path_model: MeanFreePathModel::LIQUID, + interaction_potential: vec![vec![potential]], + scattering_integral: vec![vec![scattering_integral]], + num_threads: 1, + num_chunks: 1, + use_hdf5: false, + root_finder: vec![vec![root_finder]], + track_displacements: false, + track_energy_losses: false, + }; + + #[cfg(feature = "distributions")] + let options = Options { + name: "test".to_string(), + track_trajectories: false, + track_recoils: true, + track_recoil_trajectories: false, + write_buffer_size: 8000, + weak_collision_order: 0, + suppress_deep_recoils: false, + high_energy_free_flight_paths: high_energy_free_flight_paths, + electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, + mean_free_path_model: MeanFreePathModel::LIQUID, + interaction_potential: vec![vec![potential]], + scattering_integral: vec![vec![scattering_integral]], + num_threads: 1, + num_chunks: 1, + use_hdf5: false, + root_finder: vec![vec![root_finder]], + track_displacements: false, + track_energy_losses: false, + energy_min: 0.0, + energy_max: 10.0, + energy_num: 11, + angle_min: 0.0, + angle_max: 90.0, + angle_num: 11, + x_min: 0.0, + y_min: -10.0, + z_min: -10.0, + x_max: 10.0, + y_max: 10.0, + z_max: 10.0, + x_num: 11, + y_num: 11, + z_num: 11, + }; + + let binary_collision_geometries = bca::determine_mfp_phi_impact_parameter(&mut particle_1, &material_1, &options); + + println!("Phi: {} rad p: {} Angstrom mfp: {} Angstrom", binary_collision_geometries[0].phi_azimuthal, + binary_collision_geometries[0].impact_parameter/ANGSTROM, + binary_collision_geometries[0].mfp/ANGSTROM); + + let (species_index, mut particle_2) = bca::choose_collision_partner(&mut particle_1, &material_1, + &binary_collision_geometries[0], &options); + + let mom1_0 = particle_1.get_momentum(); + let mom2_0 = particle_2.get_momentum(); + + let initial_momentum = mom1_0.add(&mom2_0); + + let binary_collision_result = bca::calculate_binary_collision(&particle_1, + &particle_2, &binary_collision_geometries[0], &options).unwrap(); + + println!("E_recoil: {} eV Psi: {} rad Psi_recoil: {} rad", binary_collision_result.recoil_energy/EV, + binary_collision_result.psi, + binary_collision_result.psi_recoil); + + println!("Initial Energies: {} eV {} eV", particle_1.E/EV, particle_2.E/EV); + + //Energy transfer to recoil + particle_2.E = binary_collision_result.recoil_energy - material_1.average_bulk_binding_energy(particle_2.pos.x, particle_2.pos.y, particle_2.pos.z); + + //Rotate particle 1, 2 by lab frame scattering angles + particle::rotate_particle(&mut particle_1, binary_collision_result.psi, + binary_collision_geometries[0].phi_azimuthal); + + particle::rotate_particle(&mut particle_2, -binary_collision_result.psi_recoil, + binary_collision_geometries[0].phi_azimuthal); + + //Subtract total energy from all simultaneous collisions and electronic stopping + bca::update_particle_energy(&mut particle_1, &material_1, 0., + binary_collision_result.recoil_energy, 0., particle_2.Z, species_index, &options); + + let mom1_1 = particle_1.get_momentum(); + let mom2_1 = particle_2.get_momentum(); + + let final_momentum = mom1_1.add(&mom2_1); + + println!("Final Energies: {} eV {} eV", particle_1.E/EV, particle_2.E/EV); + println!("X: {} {} {}% Error", initial_momentum.x/ANGSTROM/AMU, final_momentum.x/ANGSTROM/AMU, 100.*(final_momentum.x - initial_momentum.x)/initial_momentum.magnitude()); + println!("Y: {} {} {}% Error", initial_momentum.y/ANGSTROM/AMU, final_momentum.y/ANGSTROM/AMU, 100.*(final_momentum.y - initial_momentum.y)/initial_momentum.magnitude()); + println!("Z: {} {} {}% Error", initial_momentum.z/ANGSTROM/AMU, final_momentum.z/ANGSTROM/AMU, 100.*(final_momentum.z - initial_momentum.z)/initial_momentum.magnitude()); + println!(); + + assert!(approx_eq!(f64, initial_momentum.x, final_momentum.x, epsilon = 1E-12)); + assert!(approx_eq!(f64, initial_momentum.y, final_momentum.y, epsilon = 1E-12)); + assert!(approx_eq!(f64, initial_momentum.z, final_momentum.z, epsilon = 1E-12)); + + assert!(!particle_1.E.is_nan()); + assert!(!particle_2.E.is_nan()); + assert!(!initial_momentum.x.is_nan()); + assert!(!initial_momentum.x.is_nan()); + assert!(!initial_momentum.x.is_nan()); + } + } + } + } + } +} + +#[test] +fn test_rotate_particle() { + let mass = 1.; + let Z = 1.; + let E = 1.; + let Ec = 1.; + let Es = 1.; + let x = 0.; + let y = 0.; + let z = 0.; + let cosx = (PI/4.).cos(); + let cosy = (PI/4.).sin(); + let cosz = 0.; + let psi = -PI/4.; + let phi = 0.; + + let mut particle = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + //Check that rotation in 2D works + particle::rotate_particle(&mut particle, psi, phi); + assert!(approx_eq!(f64, particle.dir.x, 0., epsilon = 1E-12), "particle.dir.x: {} Should be ~0.", particle.dir.x); + assert!(approx_eq!(f64, particle.dir.y, 1., epsilon = 1E-12), "particle.dir.y: {} Should be ~1.", particle.dir.y); + + //Check that rotating back by negative psi returns to the previous values + particle::rotate_particle(&mut particle, -psi, phi); + assert!(approx_eq!(f64, particle.dir.x, cosx, epsilon = 1E-12), "particle.dir.x: {} Should be ~{}", particle.dir.x, cosx); + assert!(approx_eq!(f64, particle.dir.y, cosy, epsilon = 1E-12), "particle.dir.y: {} Should be ~{}", particle.dir.y, cosy); + + //Check that azimuthal rotation by 180 degrees works correctly + let phi = PI; + particle::rotate_particle(&mut particle, psi, phi); + assert!(approx_eq!(f64, particle.dir.x, 1., epsilon = 1E-12), "particle.dir.x: {} Should be ~1.", particle.dir.x); + assert!(approx_eq!(f64, particle.dir.y, 0., epsilon = 1E-12), "particle.dir.y: {} Should be ~0.", particle.dir.y); + + //Check that particle direction vector remains normalized following rotations + assert!(approx_eq!(f64, particle.dir.x.powi(2) + particle.dir.y.powi(2) + particle.dir.z.powi(2), 1.), "Particle direction not normalized."); + +} + +#[test] +fn test_particle_advance() { + let mass = 1.; + let Z = 1.; + let E = 1.; + let Ec = 1.; + let Es = 1.; + let x = 0.; + let y = 0.; + let z = 0.; + let cosx = (PI/4.).cos(); + let cosy = (PI/4.).sin(); + let cosz = 0.; + let mfp = 1.; + let asymptotic_deflection = 0.5; + + let mut particle = particle::Particle::new(mass, Z, E, Ec, Es, x, y, z, cosx, cosy, cosz, false, false, 0); + + let distance_traveled = particle::particle_advance(&mut particle, mfp, asymptotic_deflection); + + assert_eq!(particle.pos.x, (1. - 0.5)*cosx); + assert_eq!(particle.pos.y, (1. - 0.5)*cosy); + assert_eq!(particle.pos.z, 0.); + assert_eq!(distance_traveled, mfp - asymptotic_deflection); +} + +#[test] +fn test_quadrature() { + let Za = 1.; + let Zb = 13.; + let Ma = 1.008; + let Mb = 26.9815385; + let E0 = 10.*EV; + let p = 1.*ANGSTROM; + let a = interactions::screening_length(Za, Zb, InteractionPotential::KR_C); + + #[cfg(not(feature = "distributions"))] + let options = Options { + name: "test".to_string(), + track_trajectories: false, + track_recoils: true, + track_recoil_trajectories: false, + write_buffer_size: 8000, + weak_collision_order: 0, + suppress_deep_recoils: false, + high_energy_free_flight_paths: false, + electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, + mean_free_path_model: MeanFreePathModel::LIQUID, + interaction_potential: vec![vec![InteractionPotential::KR_C]], + scattering_integral: vec![vec![ScatteringIntegral::MENDENHALL_WELLER]], + num_threads: 1, + num_chunks: 1, + use_hdf5: false, + root_finder: vec![vec![Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-14}]], + track_displacements: false, + track_energy_losses: false, + }; + + #[cfg(feature = "distributions")] + let options = Options { + name: "test".to_string(), + track_trajectories: false, + track_recoils: true, + track_recoil_trajectories: false, + write_buffer_size: 8000, + weak_collision_order: 0, + suppress_deep_recoils: false, + high_energy_free_flight_paths: false, + electronic_stopping_mode: ElectronicStoppingMode::INTERPOLATED, + mean_free_path_model: MeanFreePathModel::LIQUID, + interaction_potential: vec![vec![InteractionPotential::KR_C]], + scattering_integral: vec![vec![ScatteringIntegral::MENDENHALL_WELLER]], + num_threads: 1, + num_chunks: 1, + use_hdf5: false, + root_finder: vec![vec![Rootfinder::NEWTON{max_iterations: 100, tolerance: 1E-14}]], + track_displacements: false, + track_energy_losses: false, + energy_min: 0.0, + energy_max: 10.0, + energy_num: 11, + angle_min: 0.0, + angle_max: 90.0, + angle_num: 11, + x_min: 0.0, + y_min: -10.0, + z_min: -10.0, + x_max: 10.0, + y_max: 10.0, + z_max: 10.0, + x_num: 11, + y_num: 11, + z_num: 11, + }; + + let x0_newton = bca::newton_rootfinder(Za, Zb, Ma, Mb, E0, p, InteractionPotential::KR_C, 100, 1E-12).unwrap(); + + //If cpr_rootfinder is enabled, compare Newton to CPR - they should be nearly identical + #[cfg(any(feature = "cpr_rootfinder_openblas", feature = "cpr_rootfinder_netlib", feature = "cpr_rootfinder_intel_mkl"))] + if let Ok(x0_cpr) = bca::cpr_rootfinder(Za, Zb, Ma, Mb, E0, p, InteractionPotential::KR_C, 2, 1000, 1E-13, 1E-16, 1E-18, 1E6, 1E-18, false) { + println!("CPR: {} Newton: {}", x0_cpr, x0_newton); + assert!(approx_eq!(f64, x0_newton, x0_cpr, epsilon=1E-3)); + }; + + //Compute center of mass deflection angle with each algorithm + let theta_gm = bca::gauss_mehler(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C, 10); + let theta_gl = bca::gauss_legendre(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C); + let theta_mw = bca::mendenhall_weller(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C); + let theta_magic = bca::magic(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C); + + //Gauss-Mehler and Gauss-Legendre should be very close to each other + assert!(approx_eq!(f64, theta_gm, theta_gl, epsilon=0.001)); + assert!(approx_eq!(f64, theta_gm, theta_mw, epsilon=0.001)); + assert!(approx_eq!(f64, theta_gm, theta_magic, epsilon=0.15)); + + println!("Gauss-Mehler: {} Gauss-Legendre: {} Mendenhall-Weller: {} MAGIC: {}", + theta_gm, theta_gl, theta_mw, theta_magic); +}