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    // Force constant K_ijk (Eq. 11-13 in UFF paper)
111    let r_ij = pi_param.r1 + pj_param.r1;
112    let r_jk = pk_param.r1 + pj_param.r1;
113    let r_ik_sq = (r_ij*r_ij + r_jk*r_jk - 2.0*r_ij*r_jk*cos_theta0).max(1e-6);
114    let r_ik = r_ik_sq.sqrt();
115    let k_ijk = 664.12 * pi_param.z_star * pk_param.z_star / (r_ik.powi(5)) * 
116                (3.0 * r_ij * r_jk * (1.0 - cos_theta0.powi(2)) - r_ik_sq * cos_theta0);
117
118    let (energy, d_e_d_cos_theta) = if (theta0_rad - std::f64::consts::PI).abs() < 1e-4 {
119        // Linear case (theta0 = 180)
120        // E = K/2 * (1 + cos(theta))
121        let energy = k_ijk * (1.0 + cos_theta);
122        let d_e_d_cos_theta = k_ijk;
123        (energy, d_e_d_cos_theta)
124    } else {
125        // Non-linear case (sp2, sp3, etc.) - Fourier expansion
126        // E = K * [C0 + C1*cos(theta) + C2*cos(2*theta)]
127        // This is more stable than harmonic (theta-theta0)^2 for large deviations
128        let sin_theta0_sq = 1.0 - cos_theta0 * cos_theta0;
129        let c2 = 1.0 / (4.0 * sin_theta0_sq);
130        let c1 = -4.0 * c2 * cos_theta0;
131        let c0 = c2 * (2.0 * cos_theta0 * cos_theta0 + 1.0);
132        
133        let cos_2theta = 2.0 * cos_theta * cos_theta - 1.0;
134        let energy = k_ijk * (c0 + c1 * cos_theta + c2 * cos_2theta);
135        // dE/d(cos_theta) = K * [C1 + 4*C2*cos(theta)]
136        let d_e_d_cos_theta = k_ijk * (c1 + 4.0 * c2 * cos_theta);
137        (energy, d_e_d_cos_theta)
138    };
139
140    // Forces: F = - dE/dx = - (dE/dcos_theta) * dcos_theta/dx
141    // d(cos_theta)/dx_i = ( (x_k - x_j)/|r_ji||r_jk| - cos_theta * (x_i - x_j)/|r_ji|^2 )
142    let grad_cos_i = (v_jk / (r_ji * r_jk)) - (v_ji * (cos_theta / (r_ji * r_ji)));
143    let grad_cos_k = (v_ji / (r_ji * r_jk)) - (v_jk * (cos_theta / (r_jk * r_jk)));
144
145    let f_i = grad_cos_i * (-d_e_d_cos_theta);
146    let f_k = grad_cos_k * (-d_e_d_cos_theta);
147    
148    // Clamp forces to prevent explosion in highly distorted structures
149    let f_i_clamped = f_i.clamp(DVec3::splat(-2000.0), DVec3::splat(2000.0));
150    let f_k_clamped = f_k.clamp(DVec3::splat(-2000.0), DVec3::splat(2000.0));
151    let f_j_clamped = -(f_i_clamped + f_k_clamped);
152    
153    Some((energy, f_i_clamped, f_j_clamped, f_k_clamped))
154}
155
156pub fn calculate_torsion(
157    b1: DVec3, b2: DVec3, b3: DVec3,
158    type_j: &UffAtomType, type_k: &UffAtomType,
159    order: f32,
160) -> Option<(f64, DVec3, DVec3, DVec3, DVec3)> {
161    let pj = get_uff_params(type_j)?;
162    let pk = get_uff_params(type_k)?;
163
164    let n1 = b1.cross(b2); let n2 = b2.cross(b3);
165    let n1_mag_sq = n1.length_squared(); let n2_mag_sq = n2.length_squared();
166    if n1_mag_sq < 1e-9 || n2_mag_sq < 1e-9 { return None; }
167
168    let m1 = n1.cross(b2) / b2.length();
169    let cos_phi = (n1.dot(n2) / (n1_mag_sq.sqrt() * n2_mag_sq.sqrt())).clamp(-1.0, 1.0);
170    let sin_phi = (m1.dot(n2) / (m1.length() * n2_mag_sq.sqrt())).clamp(-1.0, 1.0);
171    let phi = sin_phi.atan2(cos_phi);
172
173    let mut v = 5.0 * (pj.u_i * pk.u_i).sqrt();
174    let mut n_period = 3.0;
175    let phi0 = 180.0f64.to_radians();
176
177    if order > 1.2 {
178        v = 5.0 * (pj.u_i * pk.u_i).sqrt() * (1.0 + 4.18 * (order as f64 - 1.0));
179        n_period = 2.0;
180    }
181
182    let energy = 0.5 * v * (1.0 - (n_period * phi0).cos() * (n_period * phi).cos());
183    let d_e_d_phi = 0.5 * v * n_period * (n_period * phi0).cos() * (n_period * phi).sin();
184    
185    let f_i = n1 * (-d_e_d_phi * b2.length() / n1_mag_sq);
186    let f_l = n2 * (d_e_d_phi * b2.length() / n2_mag_sq);
187    let f_j = f_i * (b1.dot(b2) / b2.length_squared() - 1.0) - f_l * (b3.dot(b2) / b2.length_squared());
188    let f_k = -(f_i + f_l + f_j);
189    
190    Some((energy, f_i, f_j, f_k, f_l))
191}
192
193pub fn calculate_inversion(
194    _p0: DVec3, _p1: DVec3, _p2: DVec3, _p3: DVec3,
195    v01: DVec3, v21: DVec3, v31: DVec3,
196) -> Option<(f64, DVec3, [DVec3; 3])> {
197    let cross = v21.cross(v31);
198    if cross.length_squared() < 1e-9 { return None; }
199    let normal = cross.normalize();
200    let h = v01.dot(normal); 
201    
202    let k_inv = 40.0;
203    let energy = 0.5 * k_inv * h * h;
204    let f_j = normal * (k_inv * h); 
205    let f_others = [f_j * -0.333333, f_j * -0.333333, f_j * -0.333333];
206    
207    Some((energy, f_j, f_others))
208}