1use super::{check_energy, target, ElectronicStopping, Ion, StoppingError, ValidityRange};
43use crate::constants::{COULOMB_E2, ELECTRON_REST_ENERGY, FINE_STRUCTURE};
44use crate::units::J_PER_EV;
45use std::f64::consts::PI;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum EffectiveCharge {
50 #[default]
52 Bare,
53 BarkasEmpirical,
55}
56
57impl EffectiveCharge {
58 pub fn charge(self, z1: u8, beta: f64) -> f64 {
60 let z = f64::from(z1);
61 match self {
62 Self::Bare => z,
63 Self::BarkasEmpirical => z * (1.0 - (-125.0 * beta / z.powf(2.0 / 3.0)).exp()),
64 }
65 }
66}
67
68pub fn plasma_energy_ev(electron_density_m3: f64) -> f64 {
71 let wp2 = 4.0 * PI * electron_density_m3 * COULOMB_E2 / crate::constants::ELECTRON_MASS;
72 crate::constants::HBAR * wp2.sqrt() / J_PER_EV
73}
74
75pub fn density_effect_single_oscillator(
111 beta_gamma_sq: f64,
112 plasma_energy_ev: f64,
113 mean_excitation_ev: f64,
114) -> f64 {
115 let a = (mean_excitation_ev / plasma_energy_ev).powi(2);
116 if beta_gamma_sq <= a {
117 return 0.0;
118 }
119 (beta_gamma_sq / a).ln() - 1.0 + a / beta_gamma_sq
120}
121
122pub fn bloch_term(y: f64) -> f64 {
124 let y2 = y * y;
125 const N: u32 = 20_000;
126 let mut sum = 0.0;
127 for n in (1..=N).rev() {
129 let n = f64::from(n);
130 sum += 1.0 / (n * (n * n + y2));
131 }
132 let nn = f64::from(N);
134 sum += 1.0 / (2.0 * nn * nn);
135 -y2 * sum
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Default)]
140pub struct BetheBloch {
141 pub effective_charge: EffectiveCharge,
143 pub mean_excitation_ev: Option<f64>,
145 pub shell_over_z: f64,
147 pub density_delta: f64,
149 pub density_effect_electron_density_m3: Option<f64>,
153 pub no_bloch: bool,
155}
156
157impl BetheBloch {
158 pub fn new() -> Self {
160 Self::default()
161 }
162
163 pub fn with_effective_charge(mut self, m: EffectiveCharge) -> Self {
165 self.effective_charge = m;
166 self
167 }
168
169 pub fn with_mean_excitation_ev(mut self, i_ev: f64) -> Self {
171 self.mean_excitation_ev = Some(i_ev);
172 self
173 }
174
175 pub fn with_density_effect(mut self, electron_density_m3: f64) -> Self {
181 self.density_effect_electron_density_m3 = Some(electron_density_m3);
182 self
183 }
184
185 pub fn without_bloch(mut self) -> Self {
187 self.no_bloch = true;
188 self
189 }
190
191 pub(super) fn kinematics(ion: &Ion, energy_ev: f64) -> (f64, f64, f64) {
197 let mc2 = ion.mass_kg() * crate::constants::SPEED_OF_LIGHT.powi(2);
198 let tau = energy_ev * J_PER_EV / mc2;
199 let gamma = 1.0 + tau;
200 let beta2 = tau * (tau + 2.0) / (gamma * gamma);
201 (gamma, beta2, mc2)
202 }
203
204 fn validate(&self) -> Result<(), StoppingError> {
207 if let Some(i) = self.mean_excitation_ev {
208 if !(i.is_finite() && i > 0.0) {
209 return Err(StoppingError::InvalidParameter {
210 name: "mean_excitation_ev",
211 value: i,
212 });
213 }
214 }
215 if let Some(n) = self.density_effect_electron_density_m3 {
216 if !(n.is_finite() && n > 0.0) {
217 return Err(StoppingError::InvalidParameter {
218 name: "density_effect_electron_density_m3",
219 value: n,
220 });
221 }
222 }
223 for (name, value) in [
224 ("shell_over_z", self.shell_over_z),
225 ("density_delta", self.density_delta),
226 ] {
227 if !value.is_finite() {
228 return Err(StoppingError::InvalidParameter { name, value });
229 }
230 }
231 Ok(())
232 }
233
234 pub fn bracket(&self, ion: &Ion, target_z: u8, energy_ev: f64) -> Result<f64, StoppingError> {
236 check_energy(energy_ev)?;
237 target(target_z)?;
238 self.validate()?;
239 let (gamma, beta2, mc2_ion) = Self::kinematics(ion, energy_ev);
240 let beta = beta2.sqrt();
241 let i_j = self
242 .mean_excitation_ev
243 .unwrap_or(10.0 * f64::from(target_z))
244 * J_PER_EV;
245 let m = ELECTRON_REST_ENERGY;
246 let ratio = m / mc2_ion;
247 let wmax = 2.0 * m * beta2 * gamma * gamma / (1.0 + 2.0 * gamma * ratio + ratio * ratio);
248 let z = self.effective_charge.charge(ion.z(), beta);
249 let delta = self.density_delta
250 + self.density_effect_electron_density_m3.map_or(0.0, |n| {
251 density_effect_single_oscillator(
252 beta2 * gamma * gamma,
253 plasma_energy_ev(n),
254 i_j / J_PER_EV,
255 )
256 });
257 let mut b = 0.5 * (2.0 * m * beta2 * gamma * gamma * wmax / (i_j * i_j)).ln()
258 - beta2
259 - 0.5 * delta
260 - self.shell_over_z;
261 if !self.no_bloch {
262 b += bloch_term(z * FINE_STRUCTURE / beta);
263 }
264 if b.is_nan() {
265 return Err(StoppingError::NotApplicable {
266 model: "bethe-bloch",
267 energy_ev,
268 });
269 }
270 Ok(b)
271 }
272}
273
274impl ElectronicStopping for BetheBloch {
275 fn name(&self) -> &'static str {
276 "bethe-bloch"
277 }
278
279 fn stopping(&self, ion: &Ion, target_z: u8, energy_ev: f64) -> Result<f64, StoppingError> {
280 let b = self.bracket(ion, target_z, energy_ev)?;
281 let (_, beta2, _) = Self::kinematics(ion, energy_ev);
282 let z = self.effective_charge.charge(ion.z(), beta2.sqrt());
283 let s = 4.0 * PI * COULOMB_E2 * COULOMB_E2 * z * z * f64::from(target_z)
284 / (ELECTRON_REST_ENERGY * beta2)
285 * b;
286 if !(b > 0.0 && s.is_finite()) {
287 return Err(StoppingError::NotApplicable {
288 model: "bethe-bloch",
289 energy_ev,
290 });
291 }
292 Ok(s)
293 }
294
295 fn validity(&self, ion: &Ion) -> ValidityRange {
296 ValidityRange {
299 min_energy_ev: ion.mass_amu() * super::velocity_scale_energy_per_amu_ev(ion.z(), 3.0),
300 max_energy_ev: ion.mass_amu() * 1.0e9,
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::constants::{AVOGADRO, BOHR_RADIUS, PROTON_MASS, SPEED_OF_LIGHT};
309
310 #[test]
320 fn proton_in_si_hand_calculation() {
321 let m = BetheBloch::new()
322 .with_mean_excitation_ev(140.0)
323 .without_bloch();
324 let ion = Ion::proton();
325 let expected = [
326 (1.0e6, 9.201_572_04),
327 (1.0e7, 1.712_108_53),
328 (1.0e8, 0.281_049_35),
329 ];
330 for (e, want) in expected {
331 let got = super::super::to_ev_1e15_cm2(m.stopping(&ion, 14, e).unwrap());
332 assert!((got / want - 1.0).abs() < 2.0e-4, "{e}: {got} vs {want}");
334 }
335 }
336
337 #[test]
338 fn independent_formula_check() {
339 let re = FINE_STRUCTURE * FINE_STRUCTURE * BOHR_RADIUS;
341 let mc2_mev = ELECTRON_REST_ENERGY / J_PER_EV / 1e6;
342 let k = 4.0 * PI * AVOGADRO * (re * 100.0).powi(2) * mc2_mev; let t = 10.0;
344 let g = 1.0 + t / (PROTON_MASS * SPEED_OF_LIGHT.powi(2) / J_PER_EV / 1e6);
345 let b2 = 1.0 - 1.0 / (g * g);
346 let l = (2.0 * mc2_mev * 1e6 * b2 * g * g / 140.0).ln() - b2;
347 let per_g = k * 14.0 / 28.0855 / b2 * l; let m = BetheBloch::new()
349 .with_mean_excitation_ev(140.0)
350 .without_bloch();
351 let s = super::super::to_ev_1e15_cm2(m.stopping(&Ion::proton(), 14, t * 1e6).unwrap());
352 let per_g_model = s * 1e-15 * 1e-6 * AVOGADRO / 28.0855;
353 assert!((per_g_model / per_g - 1.0).abs() < 2e-4);
354 }
355
356 #[test]
357 fn bloch_term_small_y_matches_zeta3() {
358 let y = 0.01;
359 assert!((bloch_term(y) / (-1.202_056_903_16 * y * y) - 1.0).abs() < 1e-3);
360 }
361
362 #[test]
363 fn bloch_reduces_stopping() {
364 let ion = Ion::new(6).unwrap();
365 let a = BetheBloch::new().stopping(&ion, 14, 1.2e8).unwrap();
366 let b = BetheBloch::new()
367 .without_bloch()
368 .stopping(&ion, 14, 1.2e8)
369 .unwrap();
370 assert!(a < b);
371 }
372
373 #[test]
374 fn plasma_energy_of_aluminium() {
375 let n = 13.0 * 2.699e3 * AVOGADRO / 26.98e-3;
378 assert!((plasma_energy_ev(n) / 32.86 - 1.0).abs() < 3e-3);
379 }
380
381 #[test]
382 fn density_effect_limits() {
383 let (ehp, i): (f64, f64) = (30.0, 150.0);
384 let a = (i / ehp).powi(2);
385 assert_eq!(density_effect_single_oscillator(0.5 * a, ehp, i), 0.0);
387 let eps = 1e-4 * a;
388 let above = density_effect_single_oscillator(a + eps, ehp, i);
389 assert!((0.0..1e-6).contains(&above));
390 for bg2 in [1e6, 1e8] {
392 let want = (bg2 * (ehp / i).powi(2)).ln() - 1.0;
393 let got = density_effect_single_oscillator(bg2, ehp, i);
394 assert!((got - want).abs() < 2.0 * a / bg2 + 1e-12, "{got} {want}");
395 }
396 let mut prev = 0.0;
398 for k in 0..60 {
399 let d = density_effect_single_oscillator(a * 10f64.powf(f64::from(k) * 0.1), ehp, i);
400 assert!(d >= prev);
401 prev = d;
402 }
403 }
404
405 #[test]
406 fn density_effect_is_opt_in_and_reduces_stopping() {
407 let n = 14.0 * 2.33e3 * AVOGADRO / 28.0855e-3; let base = BetheBloch::new().with_mean_excitation_ev(173.0);
409 let on = base.with_density_effect(n);
410 let p = Ion::proton();
411 for e in [1.0e7, 1.0e9] {
414 assert_eq!(
415 base.stopping(&p, 14, e).unwrap(),
416 on.stopping(&p, 14, e).unwrap()
417 );
418 }
419 let (a, b) = (
421 base.stopping(&p, 14, 1.0e11).unwrap(),
422 on.stopping(&p, 14, 1.0e11).unwrap(),
423 );
424 assert!(b < a);
425 let mut bad = base;
426 bad.density_effect_electron_density_m3 = Some(-1.0);
427 assert!(matches!(
428 bad.stopping(&p, 14, 1.0e9),
429 Err(StoppingError::InvalidParameter { .. })
430 ));
431 }
432
433 #[test]
434 fn effective_charge_limits() {
435 assert!((EffectiveCharge::BarkasEmpirical.charge(15, 0.9) / 15.0 - 1.0).abs() < 1e-6);
436 assert!(EffectiveCharge::BarkasEmpirical.charge(15, 0.01) < 5.0);
437 }
438
439 #[test]
440 fn tiny_energy_is_a_defined_error_not_nan() {
441 let u = Ion::with_mass(92, 238.028_91).unwrap();
442 for e in [1.0e-6, 1.0e-30, 1.0e-300] {
443 for m in [BetheBloch::new(), BetheBloch::new().without_bloch()] {
444 let r = m.stopping(&u, 14, e);
445 assert!(
446 matches!(r, Err(StoppingError::NotApplicable { .. })),
447 "{e}: {r:?}"
448 );
449 assert!(!m.bracket(&u, 14, e).is_ok_and(f64::is_nan));
450 }
451 }
452 }
453
454 #[test]
455 fn beta2_is_stable_at_tiny_tau() {
456 let u = Ion::with_mass(92, 238.028_91).unwrap();
457 let (_, beta2, _) = BetheBloch::kinematics(&u, 1.0e-6);
458 assert!(beta2 > 0.0 && beta2.is_finite());
459 let mc2_ev = u.mass_kg() * crate::constants::SPEED_OF_LIGHT.powi(2) / J_PER_EV;
461 assert!((beta2 / (2.0e-6 / mc2_ev) - 1.0).abs() < 1e-12);
462 }
463
464 #[test]
465 fn invalid_parameters_are_rejected() {
466 let ion = Ion::proton();
467 for i in [0.0, -140.0, f64::NAN, f64::INFINITY] {
468 let mut m = BetheBloch::new().without_bloch();
469 m.mean_excitation_ev = Some(i);
470 assert!(matches!(
471 m.stopping(&ion, 14, 1.0e7),
472 Err(StoppingError::InvalidParameter {
473 name: "mean_excitation_ev",
474 ..
475 })
476 ));
477 assert!(matches!(
478 BetheBloch::new()
479 .with_mean_excitation_ev(i)
480 .bracket(&ion, 14, 1.0e7),
481 Err(StoppingError::InvalidParameter { .. })
482 ));
483 }
484 for bad in [f64::NAN, f64::INFINITY] {
485 let mut m = BetheBloch::new();
486 m.shell_over_z = bad;
487 assert!(matches!(
488 m.stopping(&ion, 14, 1.0e7),
489 Err(StoppingError::InvalidParameter {
490 name: "shell_over_z",
491 ..
492 })
493 ));
494 let mut m = BetheBloch::new();
495 m.density_delta = bad;
496 assert!(matches!(
497 m.stopping(&ion, 14, 1.0e7),
498 Err(StoppingError::InvalidParameter {
499 name: "density_delta",
500 ..
501 })
502 ));
503 }
504 }
505
506 #[test]
507 fn not_applicable_at_low_energy() {
508 let r = BetheBloch::new().stopping(&Ion::proton(), 14, 10.0);
509 assert!(matches!(r, Err(StoppingError::NotApplicable { .. })));
510 }
511}