1use candle_core::{DType, Result, Tensor};
20use candle_nn::{Linear, VarBuilder, encoding::one_hot, linear};
21
22pub fn relpos_encoding(residue_indices: &Tensor, n_bins: usize) -> Result<Tensor> {
36 let ri = residue_indices.to_dtype(DType::F32)?.unsqueeze(2)?; let rj = residue_indices.to_dtype(DType::F32)?.unsqueeze(1)?; let diff = rj.broadcast_sub(&ri)?; let nb = n_bins as f64;
41 let clamped = diff.clamp(-nb, nb)?;
42 let shifted = (clamped + nb)?; let shifted_u32 = shifted.to_dtype(DType::U32)?;
45 one_hot(shifted_u32, 2 * n_bins + 1, 1.0f32, 0.0f32) }
47
48pub fn chain_pair_features(chain_ids: &Tensor) -> Result<Tensor> {
56 let ci = chain_ids.to_dtype(DType::F32)?.unsqueeze(2)?; let cj = chain_ids.to_dtype(DType::F32)?.unsqueeze(1)?; let diff = ci.broadcast_sub(&cj)?; let same = diff.abs()?.lt(0.5_f64)?.to_dtype(DType::F32)?; let different = (1.0_f64 - &same)?;
62 Tensor::stack(&[&same, &different], 3) }
64
65pub fn outer_product(a: &Tensor, b: &Tensor) -> Result<Tensor> {
73 let (batch, n, da) = a.dims3()?;
74 let db = b.dim(2)?;
75 let ai = a.reshape((batch, n, 1, da, 1))?; let bj = b.reshape((batch, 1, n, 1, db))?; let prod = ai.broadcast_mul(&bj)?; prod.reshape((batch, n, n, da * db)) }
80
81pub struct PairInit {
88 relpos_proj: Linear,
89 single_proj: Linear,
90 outer_proj: Linear,
91 n_relpos_bins: usize,
92}
93
94impl PairInit {
95 pub fn load(
103 vb: VarBuilder,
104 d_single: usize,
105 d_pair: usize,
106 n_relpos_bins: usize,
107 d_outer: usize,
108 ) -> Result<Self> {
109 let relpos_in = 2 * n_relpos_bins + 1 + 2; Ok(Self {
111 relpos_proj: linear::linear_no_bias(relpos_in, d_pair, vb.pp("relpos_proj"))?,
112 single_proj: linear::linear_no_bias(d_single, d_outer, vb.pp("single_proj"))?,
113 outer_proj: linear::linear_no_bias(d_outer * d_outer, d_pair, vb.pp("outer_proj"))?,
114 n_relpos_bins,
115 })
116 }
117
118 pub fn forward(
128 &self,
129 single: &Tensor,
130 residue_indices: &Tensor,
131 chain_ids: &Tensor,
132 ) -> Result<Tensor> {
133 let relpos = relpos_encoding(residue_indices, self.n_relpos_bins)?;
135 let chain = chain_pair_features(chain_ids)?;
136 let pos_feat = Tensor::cat(&[&relpos, &chain], 3)?;
137 let pair_from_pos = pos_feat.apply(&self.relpos_proj)?;
138
139 let s_proj = single.apply(&self.single_proj)?;
141 let outer = outer_product(&s_proj, &s_proj)?;
142 let pair_from_outer = outer.apply(&self.outer_proj)?;
143
144 pair_from_pos + pair_from_outer
145 }
146}
147
148#[cfg(test)]
151mod tests {
152 use super::*;
153 use candle_core::{Device, Tensor};
154 use candle_nn::VarBuilder;
155
156 const B: usize = 1;
157 const N: usize = 8;
158 const N_BINS: usize = 32;
159
160 fn arange_indices(n: usize) -> Tensor {
161 let vals: Vec<f32> = (0..n).map(|i| i as f32).collect();
162 Tensor::from_vec(vals, &[B, N], &Device::Cpu).unwrap()
163 }
164
165 #[test]
166 fn test_relpos_encoding_shape() {
167 let idx = arange_indices(N);
168 let enc = relpos_encoding(&idx, N_BINS).unwrap();
169 assert_eq!(enc.dims(), &[B, N, N, 2 * N_BINS + 1]);
170 }
171
172 fn get_bin_vec(t: &Tensor, b: usize, i: usize, j: usize) -> Vec<f32> {
174 t.get(b)
175 .unwrap()
176 .get(i)
177 .unwrap()
178 .get(j)
179 .unwrap()
180 .to_vec1()
181 .unwrap()
182 }
183
184 fn get_pair_vec(t: &Tensor, b: usize, i: usize, j: usize) -> Vec<f32> {
186 t.get(b)
187 .unwrap()
188 .get(i)
189 .unwrap()
190 .get(j)
191 .unwrap()
192 .to_vec1()
193 .unwrap()
194 }
195
196 #[test]
197 fn test_relpos_encoding_diagonal_is_centre_bin() {
198 let idx = arange_indices(N);
199 let enc = relpos_encoding(&idx, N_BINS).unwrap();
200 for i in 0..N {
202 let vals = get_bin_vec(&enc, 0, i, i);
203 assert!(
204 (vals[N_BINS] - 1.0).abs() < 1e-5,
205 "diagonal [{i},{i}] should be hot at bin {N_BINS}, got {vals:?}"
206 );
207 let off: f32 = vals
208 .iter()
209 .enumerate()
210 .filter(|&(k, _)| k != N_BINS)
211 .map(|(_, &v)| v)
212 .sum();
213 assert!(off.abs() < 1e-5, "off bins should be zero");
214 }
215 }
216
217 #[test]
218 fn test_relpos_encoding_off_diagonal() {
219 let idx = arange_indices(N);
220 let enc = relpos_encoding(&idx, N_BINS).unwrap();
221
222 let row = get_bin_vec(&enc, 0, 0, 1);
224 assert!((row[N_BINS + 1] - 1.0).abs() < 1e-5);
225
226 let row = get_bin_vec(&enc, 0, 1, 0);
228 assert!((row[N_BINS - 1] - 1.0).abs() < 1e-5);
229 }
230
231 #[test]
232 fn test_relpos_clamped_at_boundary() {
233 let far = Tensor::from_vec(vec![0.0f32, 100.0], &[1, 2], &Device::Cpu).unwrap();
235 let enc = relpos_encoding(&far, N_BINS).unwrap();
236 let row = get_bin_vec(&enc, 0, 0, 1);
237 assert!(
238 (row[2 * N_BINS] - 1.0).abs() < 1e-5,
239 "should clamp to max bin"
240 );
241 }
242
243 #[test]
244 fn test_chain_pair_features_shape() {
245 let chain_ids = Tensor::zeros(&[B, N], DType::F32, &Device::Cpu).unwrap();
246 let feats = chain_pair_features(&chain_ids).unwrap();
247 assert_eq!(feats.dims(), &[B, N, N, 2]);
248 }
249
250 #[test]
251 fn test_chain_pair_features_same_chain() {
252 let chain_ids = Tensor::zeros(&[B, N], DType::F32, &Device::Cpu).unwrap();
254 let feats = chain_pair_features(&chain_ids).unwrap();
255 let val = get_pair_vec(&feats, 0, 0, 1);
256 assert!(
257 (val[0] - 1.0).abs() < 1e-5,
258 "same chain index 0 should be 1"
259 );
260 assert!(val[1].abs() < 1e-5, "same chain index 1 should be 0");
261 }
262
263 #[test]
264 fn test_chain_pair_features_cross_chain() {
265 let ids: Vec<f32> = (0..N).map(|i| if i < 4 { 0.0 } else { 1.0 }).collect();
267 let chain_ids = Tensor::from_vec(ids, &[B, N], &Device::Cpu).unwrap();
268 let feats = chain_pair_features(&chain_ids).unwrap();
269
270 let same = get_pair_vec(&feats, 0, 0, 1);
272 assert!((same[0] - 1.0).abs() < 1e-5);
273 assert!(same[1].abs() < 1e-5);
274
275 let cross = get_pair_vec(&feats, 0, 0, 4);
277 assert!(cross[0].abs() < 1e-5);
278 assert!((cross[1] - 1.0).abs() < 1e-5);
279 }
280
281 #[test]
282 fn test_outer_product_shape() {
283 let a = Tensor::zeros(&[B, N, 16], DType::F32, &Device::Cpu).unwrap();
284 let b = Tensor::zeros(&[B, N, 16], DType::F32, &Device::Cpu).unwrap();
285 let out = outer_product(&a, &b).unwrap();
286 assert_eq!(out.dims(), &[B, N, N, 256]);
287 }
288
289 #[test]
290 fn test_outer_product_values() {
291 let a = Tensor::from_vec(vec![1.0f32, 2.0], &[1, 1, 2], &Device::Cpu).unwrap();
293 let b = Tensor::from_vec(vec![3.0f32, 4.0], &[1, 1, 2], &Device::Cpu).unwrap();
294 let out = outer_product(&a, &b).unwrap();
295 let vals = out.flatten_all().unwrap().to_vec1::<f32>().unwrap();
296 assert!((vals[0] - 3.0).abs() < 1e-5, "1*3 = 3");
297 assert!((vals[1] - 4.0).abs() < 1e-5, "1*4 = 4");
298 assert!((vals[2] - 6.0).abs() < 1e-5, "2*3 = 6");
299 assert!((vals[3] - 8.0).abs() < 1e-5, "2*4 = 8");
300 }
301
302 #[test]
303 fn test_pair_init_forward_shape() {
304 let device = Device::Cpu;
305 let d_single = 32_usize; let d_pair = 16_usize;
307 let n_bins = 4_usize;
308 let d_outer = 4_usize;
309
310 let vb = VarBuilder::zeros(DType::F32, &device);
311 let init = PairInit::load(vb, d_single, d_pair, n_bins, d_outer).unwrap();
312
313 let single = Tensor::zeros(&[B, N, d_single], DType::F32, &device).unwrap();
314 let residue_idx = arange_indices(N);
315 let chain_ids = Tensor::zeros(&[B, N], DType::F32, &device).unwrap();
316
317 let pair = init.forward(&single, &residue_idx, &chain_ids).unwrap();
318 assert_eq!(pair.dims(), &[B, N, N, d_pair]);
319 }
320}