Skip to main content

uff_relax/forcefield/
interactions.rs

1use glam::DVec3;
2use crate::params::get_uff_params;
3use crate::atom::UffAtomType;
4
5/// Result of a single interaction calculation
6pub struct InteractionResult {
7    pub energy: f64,
8    pub forces: Vec<(usize, DVec3)>,
9}
10
11pub fn calculate_bond(
12    _i: usize, 
13    _j: usize, 
14    _pos_i: DVec3, 
15    _pos_j: DVec3, 
16    order: f32, 
17    type_i: &UffAtomType, 
18    type_j: &UffAtomType,
19    dist_vec: DVec3,
20) -> Option<(f64, DVec3)> {
21    let dist = dist_vec.length();
22    if dist < 1e-6 { return None; }
23
24    let pi = get_uff_params(type_i)?;
25    let pj = get_uff_params(type_j)?;
26
27    let r_bo = -0.1332 * (pi.r1 + pj.r1) * (order as f64).ln().max(0.0);
28    let r_en = pi.r1 * pj.r1 * (pi.chi.sqrt() - pj.chi.sqrt()).powi(2) / (pi.chi * pi.r1 + pj.chi * pj.r1);
29    let r0 = pi.r1 + pj.r1 + r_bo - r_en;
30
31    let k = 664.12 * pi.z_star * pj.z_star / (r0.powi(3));
32    let dr = dist - r0;
33    let energy = 0.5 * k * dr * dr;
34    
35    let force_mag = -k * dr;
36    let f_vec = dist_vec.normalize() * force_mag.clamp(-1000.0, 1000.0);
37    
38    Some((energy, f_vec))
39}
40
41pub fn calculate_lj(
42    dist_vec: DVec3,
43    type_i: &UffAtomType,
44    type_j: &UffAtomType,
45    cutoff_sq: f64,
46    scale: f64,
47) -> Option<(f64, DVec3)> {
48    let dist_sq = dist_vec.length_squared();
49    if dist_sq < cutoff_sq && dist_sq > 1e-12 {
50        let dist = dist_sq.sqrt();
51        let pi = get_uff_params(type_i)?;
52        let pj = get_uff_params(type_j)?;
53        let x_ij = (pi.x1 * pj.x1).sqrt();
54        let d_ij = (pi.d1 * pj.d1).sqrt() * scale;
55        
56        let d = dist.max(x_ij * 0.1); 
57        
58        let r_ratio = x_ij / d;
59        let r_ratio_6 = r_ratio.powi(6);
60        let r_ratio_12 = r_ratio_6 * r_ratio_6;
61        
62        let raw_energy = d_ij * (r_ratio_12 - 2.0 * r_ratio_6);
63        let raw_f_mag = 12.0 * d_ij / d * (r_ratio_12 - r_ratio_6);
64        
65        // Switching function (5th order polynomial)
66        let r_off = cutoff_sq.sqrt();
67        let r_on = 0.9 * r_off;
68        
69        let (sw, dsw) = if d > r_on {
70            let x = (d - r_on) / (r_off - r_on);
71            let sw = 1.0 - 10.0*x.powi(3) + 15.0*x.powi(4) - 6.0*x.powi(5);
72            let dsw = (-30.0*x.powi(2) + 60.0*x.powi(3) - 30.0*x.powi(4)) / (r_off - r_on);
73            (sw, dsw)
74        } else {
75            (1.0, 0.0)
76        };
77        
78        let energy = raw_energy * sw;
79        let f_mag = raw_f_mag * sw - raw_energy * dsw;
80        
81        // High clamp for repulsion to prevent overlap, lower clamp for attraction to keep stability
82        let clamped_f_mag = if f_mag > 0.0 {
83            f_mag.min(10000.0) 
84        } else {
85            f_mag.max(-500.0)
86        };
87        
88        let f_vec = dist_vec.normalize() * clamped_f_mag;
89        return Some((energy, f_vec));
90    }
91    None
92}
93
94pub fn calculate_angle(
95    _pos_i: DVec3, _pos_j: DVec3, _pos_k: DVec3,
96    type_i: &UffAtomType, type_j: &UffAtomType, type_k: &UffAtomType,
97    v_ji: DVec3, v_jk: DVec3,
98) -> Option<(f64, DVec3, DVec3, DVec3)> {
99    let pi_param = get_uff_params(type_i)?;
100    let pj_param = get_uff_params(type_j)?;
101    let pk_param = get_uff_params(type_k)?;
102
103    let r_ji = v_ji.length(); let r_jk = v_jk.length();
104    if r_ji < 1e-6 || r_jk < 1e-6 { return None; }
105
106    let cos_theta = (v_ji.dot(v_jk) / (r_ji * r_jk)).clamp(-1.0, 1.0);
107    let theta0_rad = pj_param.theta0.to_radians();
108    let cos_theta0 = theta0_rad.cos();
109
110    let r0_ij = pi_param.r1 + pj_param.r1;
111    let r0_jk = pk_param.r1 + pj_param.r1;
112    let r0_ik_sq = (r0_ij*r0_ij + r0_jk*r0_jk - 2.0*r0_ij*r0_jk*cos_theta0).max(1e-6);
113    let r0_ik = r0_ik_sq.sqrt();
114    let k_ijk = 664.12 * pi_param.z_star * pk_param.z_star / (r0_ik.powi(5)) * 
115                (3.0 * r0_ij * r0_jk * (1.0 - cos_theta0.powi(2)) - r0_ik_sq * cos_theta0);
116
117    let theta = cos_theta.acos();
118    let sin_theta = (1.0 - cos_theta * cos_theta).sqrt().max(1e-6);
119    let d_e_d_theta = k_ijk * (theta - theta0_rad);
120    let energy = 0.5 * k_ijk * (theta - theta0_rad).powi(2);
121
122    let grad_theta_i = ((v_ji * cos_theta / (r_ji * r_ji)) - (v_jk / (r_ji * r_jk))) / sin_theta;
123    let grad_theta_k = ((v_jk * cos_theta / (r_jk * r_jk)) - (v_ji / (r_ji * r_jk))) / sin_theta;
124
125    let f_i = grad_theta_i * (-d_e_d_theta);
126    let f_k = grad_theta_k * (-d_e_d_theta);
127    let f_j = -(f_i + f_k);
128    
129    Some((energy, f_i, f_j, f_k))
130}
131
132pub fn calculate_torsion(
133    b1: DVec3, b2: DVec3, b3: DVec3,
134    type_j: &UffAtomType, type_k: &UffAtomType,
135    order: f32,
136) -> Option<(f64, DVec3, DVec3, DVec3, DVec3)> {
137    let pj = get_uff_params(type_j)?;
138    let pk = get_uff_params(type_k)?;
139
140    let n1 = b1.cross(b2); let n2 = b2.cross(b3);
141    let n1_mag_sq = n1.length_squared(); let n2_mag_sq = n2.length_squared();
142    if n1_mag_sq < 1e-9 || n2_mag_sq < 1e-9 { return None; }
143
144    let m1 = n1.cross(b2) / b2.length();
145    let cos_phi = (n1.dot(n2) / (n1_mag_sq.sqrt() * n2_mag_sq.sqrt())).clamp(-1.0, 1.0);
146    let sin_phi = (m1.dot(n2) / (m1.length() * n2_mag_sq.sqrt())).clamp(-1.0, 1.0);
147    let phi = sin_phi.atan2(cos_phi);
148
149    let mut v = 5.0 * (pj.u_i * pk.u_i).sqrt();
150    let mut n_period = 3.0;
151    let phi0 = 180.0f64.to_radians();
152
153    if order > 1.2 {
154        v = 5.0 * (pj.u_i * pk.u_i).sqrt() * (1.0 + 4.18 * (order as f64 - 1.0));
155        n_period = 2.0;
156    }
157
158    let energy = 0.5 * v * (1.0 - (n_period * phi0).cos() * (n_period * phi).cos());
159    let d_e_d_phi = 0.5 * v * n_period * (n_period * phi0).cos() * (n_period * phi).sin();
160    
161    let f_i = n1 * (-d_e_d_phi * b2.length() / n1_mag_sq);
162    let f_l = n2 * (d_e_d_phi * b2.length() / n2_mag_sq);
163    let f_j = f_i * (b1.dot(b2) / b2.length_squared() - 1.0) - f_l * (b3.dot(b2) / b2.length_squared());
164    let f_k = -(f_i + f_l + f_j);
165    
166    Some((energy, f_i, f_j, f_k, f_l))
167}
168
169pub fn calculate_inversion(
170    _p0: DVec3, _p1: DVec3, _p2: DVec3, _p3: DVec3,
171    v01: DVec3, v21: DVec3, v31: DVec3,
172) -> Option<(f64, DVec3, [DVec3; 3])> {
173    let cross = v21.cross(v31);
174    if cross.length_squared() < 1e-9 { return None; }
175    let normal = cross.normalize();
176    let h = v01.dot(normal); 
177    
178    let k_inv = 40.0;
179    let energy = 0.5 * k_inv * h * h;
180    let f_j = normal * (k_inv * h); 
181    let f_others = [f_j * -0.333333, f_j * -0.333333, f_j * -0.333333];
182    
183    Some((energy, f_j, f_others))
184}