darkmatter 0.0.2

Dark matter simulation engine — gravitational fields, particle dynamics, halo stability, and cosmological constants
Documentation
use std::f64::consts::PI;

pub fn quenching_factor_lindhard(e_r_kev: f64, z: u32, a: u32) -> f64 {
    let k = 0.133 * (z as f64).powf(2.0 / 3.0) * (a as f64).powf(-0.5);
    let epsilon = 11.5 * e_r_kev * (z as f64).powf(-7.0 / 3.0);
    let g = 3.0 * epsilon.powf(0.15) + 0.7 * epsilon.powf(0.6) + epsilon;
    k * g / (1.0 + k * g)
}

pub fn quenching_factor_xenon(e_r_kev: f64) -> f64 {
    quenching_factor_lindhard(e_r_kev, 54, 131)
}

pub fn quenching_factor_germanium(e_r_kev: f64) -> f64 {
    quenching_factor_lindhard(e_r_kev, 32, 73)
}

pub fn quenching_factor_sodium(e_r_kev: f64) -> f64 {
    0.3 * (1.0 + 0.01 * e_r_kev) / (1.0 + 0.05 * e_r_kev)
}

pub fn quenching_factor_iodine(e_r_kev: f64) -> f64 {
    0.09 * (1.0 + 0.004 * e_r_kev) / (1.0 + 0.02 * e_r_kev)
}

pub fn nuclear_response_m0(q_fm_inv: f64, a: u32, z: u32) -> f64 {
    let r = effective_nuclear_radius_fm(a);
    let qr = q_fm_inv * r;
    let ff = helm_ff_squared(qr, a);
    let coherent = (z as f64).powi(2) + ((a - z) as f64).powi(2);
    coherent * ff
}

pub fn nuclear_response_m1(q_fm_inv: f64, a: u32, z: u32) -> f64 {
    let r = effective_nuclear_radius_fm(a);
    let qr = q_fm_inv * r;
    let ff = helm_ff_squared(qr, a);
    let q_gev = q_fm_inv * 0.197;
    let m_n = 0.938;
    (z as f64 - (a - z) as f64).powi(2) * ff * (q_gev / (2.0 * m_n)).powi(2)
}

fn helm_ff_squared(qr: f64, a: u32) -> f64 {
    if qr.abs() < 1e-10 {
        return 1.0;
    }
    let s = 0.9;
    let rn = effective_nuclear_radius_fm(a);
    let j1_over_qr = (qr.sin() - qr * qr.cos()) / (qr * qr * qr);
    let val = 3.0 * j1_over_qr * (-0.5 * (qr * s / rn).powi(2)).exp();
    val * val
}

fn effective_nuclear_radius_fm(a: u32) -> f64 {
    let c = 1.23 * (a as f64).powf(1.0 / 3.0) - 0.6;
    let a_skin = 0.52;
    let s = 0.9;
    (c * c + 7.0 / 3.0 * PI * PI * a_skin * a_skin - 5.0 * s * s).sqrt()
}

pub fn spin_structure_proton_xe131() -> (f64, f64, f64) {
    (0.5, 0.010, -0.227)
}

pub fn spin_structure_neutron_xe131() -> (f64, f64, f64) {
    (0.5, -0.009, 0.339)
}

pub fn spin_structure_proton_ge73() -> (f64, f64, f64) {
    (4.5, 0.030, 0.378)
}

pub fn spin_structure_fluorine19() -> (f64, f64, f64) {
    (0.5, 0.477, -0.004)
}

pub fn diurnal_modulation_velocity_shift(
    v_lab_km_s: f64,
    v_earth_rot_km_s: f64,
    hour: f64,
    latitude_deg: f64,
) -> f64 {
    let lat = latitude_deg * PI / 180.0;
    let omega = 2.0 * PI * hour / 24.0;
    let delta_v = v_earth_rot_km_s * lat.cos() * omega.cos();
    v_lab_km_s + delta_v
}

pub fn annual_modulation_amplitude(
    m_dm_gev: f64,
    m_nucleus_gev: f64,
    sigma_cm2: f64,
    e_r_kev: f64,
    v_0_km_s: f64,
    v_earth_km_s: f64,
) -> f64 {
    let mu = m_dm_gev * m_nucleus_gev / (m_dm_gev + m_nucleus_gev);
    let e_r_gev = e_r_kev * 1e-6;
    let v_min = (m_nucleus_gev * e_r_gev / (2.0 * mu * mu)).sqrt() * 3e8 / 1e3;
    let v_0 = v_0_km_s;

    let eta_plus = (-((v_min) / (v_0 + v_earth_km_s)).powi(2)).exp();
    let eta_minus = (-((v_min) / (v_0 - v_earth_km_s)).powi(2)).exp();
    sigma_cm2 * (eta_plus - eta_minus).abs() / 2.0
}

pub fn directional_recoil_rate(
    cos_gamma: f64,
    v_lab_km_s: f64,
    v_0_km_s: f64,
    v_min_km_s: f64,
) -> f64 {
    let v_lab = v_lab_km_s * 1e3;
    let v_0 = v_0_km_s * 1e3;
    let v_min = v_min_km_s * 1e3;
    let v_eff = v_lab * cos_gamma;
    let exponent = -((v_min - v_eff).powi(2)) / (v_0 * v_0);
    if exponent < -50.0 {
        return 0.0;
    }
    exponent.exp() / (PI * v_0 * v_0).sqrt()
}

pub fn head_tail_asymmetry(v_lab_km_s: f64, v_0_km_s: f64) -> f64 {
    let x = v_lab_km_s / v_0_km_s;
    let forward = (-((1.0 - x) / 1.0).powi(2)).exp();
    let backward = (-((1.0 + x) / 1.0).powi(2)).exp();
    (forward - backward) / (forward + backward)
}

pub fn neutrino_floor_xe_cm2(m_dm_gev: f64) -> f64 {
    if m_dm_gev < 6.0 {
        return 1e-45 * (6.0 / m_dm_gev).powi(2);
    }
    if m_dm_gev < 10.0 {
        return 4e-46;
    }
    if m_dm_gev < 100.0 {
        return 2e-48 * (m_dm_gev / 10.0);
    }
    2e-47
}

pub fn neutrino_floor_ge_cm2(m_dm_gev: f64) -> f64 {
    if m_dm_gev < 4.0 {
        return 5e-44 * (4.0 / m_dm_gev).powi(2);
    }
    if m_dm_gev < 8.0 {
        return 1e-44;
    }
    if m_dm_gev < 100.0 {
        return 5e-47 * (m_dm_gev / 8.0);
    }
    6e-46
}

pub fn minimum_detectable_mass_gev(
    e_threshold_kev: f64,
    m_nucleus_gev: f64,
    v_esc_km_s: f64,
) -> f64 {
    let e_thr_gev = e_threshold_kev * 1e-6;
    let v_max = v_esc_km_s * 1e3;
    let c = 3e8;
    m_nucleus_gev * e_thr_gev / (2.0 * v_max * v_max / (c * c) * m_nucleus_gev)
}