|
| 1 | +# Copyright 2024 ByteDance and/or its affiliates. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
1 | 15 | import unittest |
| 16 | + |
2 | 17 | import torch |
| 18 | + |
3 | 19 | from protenix.model.sample_confidence import calculate_chain_pair_pae |
4 | 20 |
|
5 | | -class TestSampleConfidence(unittest.TestCase): |
6 | | - def test_calculate_chain_pair_pae(self): |
7 | | - # Setup inputs |
8 | | - N_samples = 2 |
| 21 | + |
| 22 | +class TestCalculateChainPairPAE(unittest.TestCase): |
| 23 | + def test_basic_two_chains(self): |
| 24 | + """Test basic case with two chains""" |
| 25 | + N_sample = 1 |
| 26 | + N_token = 6 |
| 27 | + |
| 28 | + token_pair_pae = torch.zeros(N_sample, N_token, N_token) |
| 29 | + token_pair_pae[0, :3, :3] = 1.0 # Chain 0 internal |
| 30 | + token_pair_pae[0, 3:, 3:] = 2.0 # Chain 1 internal |
| 31 | + token_pair_pae[0, :3, 3:] = 3.0 # Chain 0->1 |
| 32 | + token_pair_pae[0, 3:, :3] = 4.0 # Chain 1->0 |
| 33 | + |
| 34 | + asym_id = torch.tensor([0, 0, 0, 1, 1, 1], dtype=torch.long) |
| 35 | + token_has_frame = torch.ones(N_token, dtype=torch.bool) |
| 36 | + |
| 37 | + result = calculate_chain_pair_pae(token_pair_pae, asym_id, token_has_frame) |
| 38 | + |
| 39 | + self.assertIn("chain_pair_pae_mean", result) |
| 40 | + self.assertIn("chain_pair_pae_min", result) |
| 41 | + |
| 42 | + mean = result["chain_pair_pae_mean"] |
| 43 | + min_val = result["chain_pair_pae_min"] |
| 44 | + |
| 45 | + self.assertEqual(mean.shape, (N_sample, 2, 2)) |
| 46 | + self.assertEqual(min_val.shape, (N_sample, 2, 2)) |
| 47 | + |
| 48 | + # Check cross-chain PAE - the function computes separate 0->1 and 1->0 |
| 49 | + # and stores them in respective positions (not symmetric) |
| 50 | + self.assertTrue(torch.allclose(mean[0, 0, 1], torch.tensor(3.0))) |
| 51 | + self.assertTrue(torch.allclose(min_val[0, 0, 1], torch.tensor(3.0))) |
| 52 | + self.assertTrue(torch.allclose(mean[0, 1, 0], torch.tensor(4.0))) |
| 53 | + self.assertTrue(torch.allclose(min_val[0, 1, 0], torch.tensor(4.0))) |
| 54 | + |
| 55 | + def test_with_contact_probs(self): |
| 56 | + """Test with custom contact probabilities""" |
| 57 | + N_sample = 1 |
9 | 58 | N_token = 4 |
10 | | - N_chain = 2 |
11 | 59 |
|
12 | | - # Asymmetric IDs: two chains, 2 tokens each |
13 | | - asym_id = torch.tensor([0, 0, 1, 1], dtype=torch.long) |
| 60 | + token_pair_pae = torch.zeros(N_sample, N_token, N_token) |
| 61 | + token_pair_pae[0, :2, 2:] = 5.0 |
| 62 | + token_pair_pae[0, 2:, :2] = 10.0 |
14 | 63 |
|
15 | | - # All tokens have frames |
16 | | - token_has_frame = torch.tensor([True, True, True, False], dtype=torch.bool) |
17 | | - |
18 | | - # Token pair PAE: [N_samples, N_token, N_token] |
19 | | - # Sample 0 |
20 | | - token_pair_pae_s0 = torch.tensor([ |
21 | | - [1.0, 2.0, 3.0, 4.0], |
22 | | - [2.0, 1.0, 4.0, 3.0], |
23 | | - [3.0, 4.0, 1.0, 2.0], |
24 | | - [4.0, 3.0, 2.0, 1.0], |
25 | | - ]) |
26 | | - # Sample 1 |
27 | | - token_pair_pae_s1 = torch.tensor([ |
28 | | - [2.0, 3.0, 4.0, 5.0], |
29 | | - [3.0, 2.0, 5.0, 4.0], |
30 | | - [4.0, 5.0, 2.0, 3.0], |
31 | | - [5.0, 4.0, 3.0, 2.0], |
32 | | - ]) |
33 | | - token_pair_pae = torch.stack([token_pair_pae_s0, token_pair_pae_s1]) |
34 | | - |
35 | | - # Expected output shapes |
36 | | - # chain_pair_pae_mean: [N_samples, N_chain, N_chain] |
37 | | - # chain_pair_pae_min: [N_samples, N_chain, N_chain] |
| 64 | + contact_probs = torch.zeros(N_token, N_token) |
| 65 | + contact_probs[:2, 2:] = 0.1 |
| 66 | + contact_probs[2:, :2] = 0.9 |
| 67 | + |
| 68 | + asym_id = torch.tensor([0, 0, 1, 1], dtype=torch.long) |
| 69 | + token_has_frame = torch.ones(N_token, dtype=torch.bool) |
38 | 70 |
|
39 | 71 | result = calculate_chain_pair_pae( |
40 | | - token_pair_pae=token_pair_pae, |
41 | | - asym_id=asym_id, |
42 | | - token_has_frame=token_has_frame, |
| 72 | + token_pair_pae, asym_id, token_has_frame, contact_probs=contact_probs |
43 | 73 | ) |
44 | 74 |
|
45 | | - self.assertIn("chain_pair_pae_mean", result) |
46 | | - self.assertIn("chain_pair_pae_min", result) |
| 75 | + mean = result["chain_pair_pae_mean"] |
| 76 | + |
| 77 | + # chain_pair_pae_mean[0, a, b] only uses contact_probs[a_mask, b_mask] |
| 78 | + # So 0->1 uses contact_probs[:2, 2:] = 0.1 |
| 79 | + self.assertTrue(torch.allclose(mean[0, 0, 1], torch.tensor(5.0))) |
| 80 | + # 1->0 uses contact_probs[2:, :2] = 0.9 |
| 81 | + self.assertTrue(torch.allclose(mean[0, 1, 0], torch.tensor(10.0))) |
| 82 | + |
| 83 | + def test_single_chain(self): |
| 84 | + """Test with only one chain""" |
| 85 | + N_sample = 1 |
| 86 | + N_token = 4 |
| 87 | + |
| 88 | + token_pair_pae = torch.ones(N_sample, N_token, N_token) |
| 89 | + asym_id = torch.zeros(N_token, dtype=torch.long) |
| 90 | + token_has_frame = torch.ones(N_token, dtype=torch.bool) |
| 91 | + |
| 92 | + result = calculate_chain_pair_pae(token_pair_pae, asym_id, token_has_frame) |
| 93 | + |
| 94 | + mean = result["chain_pair_pae_mean"] |
| 95 | + min_val = result["chain_pair_pae_min"] |
47 | 96 |
|
48 | | - chain_pair_pae_mean = result["chain_pair_pae_mean"] |
| 97 | + self.assertEqual(mean.shape, (N_sample, 1, 1)) |
| 98 | + self.assertEqual(min_val.shape, (N_sample, 1, 1)) |
| 99 | + |
| 100 | + def test_no_valid_tokens(self): |
| 101 | + """Test with no tokens having frame""" |
| 102 | + N_sample = 1 |
| 103 | + N_token = 4 |
| 104 | + |
| 105 | + token_pair_pae = torch.ones(N_sample, N_token, N_token) |
| 106 | + asym_id = torch.tensor([0, 0, 1, 1], dtype=torch.long) |
| 107 | + token_has_frame = torch.zeros(N_token, dtype=torch.bool) |
| 108 | + |
| 109 | + result = calculate_chain_pair_pae(token_pair_pae, asym_id, token_has_frame) |
| 110 | + |
| 111 | + mean = result["chain_pair_pae_mean"] |
| 112 | + min_val = result["chain_pair_pae_min"] |
| 113 | + |
| 114 | + self.assertTrue(torch.isnan(mean[0, 0, 1])) |
| 115 | + self.assertTrue(torch.isnan(min_val[0, 0, 1])) |
| 116 | + |
| 117 | + def test_multiple_samples(self): |
| 118 | + """Test with multiple samples""" |
| 119 | + N_sample = 2 |
| 120 | + N_token = 4 |
| 121 | + |
| 122 | + token_pair_pae = torch.zeros(N_sample, N_token, N_token) |
| 123 | + token_pair_pae[0, :2, 2:] = 1.0 |
| 124 | + token_pair_pae[0, 2:, :2] = 2.0 |
| 125 | + token_pair_pae[1, :2, 2:] = 3.0 |
| 126 | + token_pair_pae[1, 2:, :2] = 4.0 |
| 127 | + |
| 128 | + asym_id = torch.tensor([0, 0, 1, 1], dtype=torch.long) |
| 129 | + token_has_frame = torch.ones(N_token, dtype=torch.bool) |
| 130 | + |
| 131 | + result = calculate_chain_pair_pae(token_pair_pae, asym_id, token_has_frame) |
| 132 | + |
| 133 | + mean = result["chain_pair_pae_mean"] |
| 134 | + |
| 135 | + self.assertTrue(torch.allclose(mean[0, 0, 1], torch.tensor(1.0))) |
| 136 | + self.assertTrue(torch.allclose(mean[0, 1, 0], torch.tensor(2.0))) |
| 137 | + self.assertTrue(torch.allclose(mean[1, 0, 1], torch.tensor(3.0))) |
| 138 | + self.assertTrue(torch.allclose(mean[1, 1, 0], torch.tensor(4.0))) |
| 139 | + |
| 140 | + def test_gapped_asym_id(self): |
| 141 | + """Test with non-contiguous asym_id""" |
| 142 | + N_sample = 1 |
| 143 | + N_token = 4 |
| 144 | + |
| 145 | + token_pair_pae = torch.ones(N_sample, N_token, N_token) |
| 146 | + asym_id = torch.tensor([1, 1, 3, 3], dtype=torch.long) # Gapped IDs |
| 147 | + token_has_frame = torch.ones(N_token, dtype=torch.bool) |
| 148 | + |
| 149 | + result = calculate_chain_pair_pae(token_pair_pae, asym_id, token_has_frame) |
| 150 | + |
| 151 | + mean = result["chain_pair_pae_mean"] |
| 152 | + self.assertEqual(mean.shape, (N_sample, 2, 2)) # Should remap to 0 and 1 |
| 153 | + |
| 154 | + |
| 155 | +class TestSampleConfidence(unittest.TestCase): |
| 156 | + def test_calculate_chain_pair_pae(self): |
| 157 | + """Existing test case preserved for backward compatibility""" |
| 158 | + N_sample = 1 |
| 159 | + N_token = 6 |
| 160 | + token_pair_pae = torch.ones(N_sample, N_token, N_token) |
| 161 | + token_pair_pae[0, :3, 3:] = 1.0 |
| 162 | + token_pair_pae[0, 3:, :3] = 1.0 |
| 163 | + asym_id = torch.tensor([0, 0, 0, 1, 1, 1], dtype=torch.long) |
| 164 | + token_has_frame = torch.ones(N_token, dtype=torch.bool) |
| 165 | + |
| 166 | + result = calculate_chain_pair_pae(token_pair_pae, asym_id, token_has_frame) |
49 | 167 | chain_pair_pae_min = result["chain_pair_pae_min"] |
50 | 168 |
|
51 | | - self.assertEqual(chain_pair_pae_mean.shape, (N_samples, N_chain, N_chain)) |
52 | | - self.assertEqual(chain_pair_pae_min.shape, (N_samples, N_chain, N_chain)) |
53 | | - |
54 | | - # Check chain 1 vs chain 1 (Sample 0) |
55 | | - # For chain 1 vs chain 1, the valid tokens are only token 2 (token 3 has frame=False) |
56 | | - # token_pair_pae_s0[2, 2] = 1.0 |
57 | | - # min = 1.0, mean = 1.0 |
58 | | - self.assertTrue(torch.allclose(chain_pair_pae_min[0, 1, 1], torch.tensor(1.0))) |
59 | | - self.assertTrue(torch.allclose(chain_pair_pae_mean[0, 1, 1], torch.tensor(1.0))) |
60 | | - |
61 | | - # Check chain 0 vs chain 1 (Sample 0) |
62 | | - # For chain 0 vs chain 1, valid tokens for chain 0 are 0, 1. For chain 1 is 2. |
63 | | - # sub_pae for sample 0 is token_pair_pae_s0[0:2, 2] -> [3.0, 4.0] |
64 | | - # min = min(3.0, 4.0) = 3.0 |
65 | | - # mean = (3.0 + 4.0) / 2 = 3.5 |
66 | | - self.assertTrue(torch.allclose(chain_pair_pae_min[0, 0, 1], torch.tensor(3.0))) |
67 | | - self.assertTrue(torch.allclose(chain_pair_pae_mean[0, 0, 1], torch.tensor(3.5))) |
| 169 | + self.assertEqual(chain_pair_pae_min.shape, (N_sample, 2, 2)) |
| 170 | + self.assertTrue(torch.allclose(chain_pair_pae_min[0, 0, 1], torch.tensor(1.0))) |
| 171 | + |
68 | 172 |
|
69 | 173 | if __name__ == "__main__": |
70 | 174 | unittest.main() |
| 175 | + |
0 commit comments