wuxing0105 commited on
Commit
62d3300
·
verified ·
1 Parent(s): 40d185e

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. flax_model/alphafold3/_data/libcifpp/mmcif_ddl.dic +0 -0
  2. flax_model/alphafold3/_data/libcifpp/mmcif_ma.dic +0 -0
  3. flax_model/alphafold3/_data/libcifpp/mmcif_pdbx.dic +0 -0
  4. flax_model/alphafold3/_tools/hmmer/.built +1 -0
  5. flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-compstruct.1 +134 -0
  6. flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-construct.1 +196 -0
  7. flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-seqrange.1 +99 -0
  8. flax_model/alphafold3/data/tools/hmmalign.py +134 -0
  9. flax_model/alphafold3/jax/attention/attention.py +150 -0
  10. flax_model/alphafold3/jax/common/array_view.py +397 -0
  11. flax_model/alphafold3/jax/common/precision.py +84 -0
  12. flax_model/alphafold3/jax/gated_linear_unit/__init__.py +0 -0
  13. flax_model/alphafold3/jax/gated_linear_unit/gated_linear_unit_base.py +122 -0
  14. flax_model/alphafold3/jax/gated_linear_unit/matmul_ext.py +265 -0
  15. flax_model/alphafold3/jax/geometry/rigid_matrix_vector.py +216 -0
  16. flax_model/alphafold3/jax/geometry/rotation_matrix.py +283 -0
  17. flax_model/alphafold3/jax/geometry/struct_of_array.py +223 -0
  18. flax_model/alphafold3/jax/geometry/utils.py +135 -0
  19. flax_model/alphafold3/jax/geometry/vector.py +214 -0
  20. flax_model/alphafold3/model/__init__.py +0 -0
  21. flax_model/alphafold3/model/atom_layout/__init__.py +0 -0
  22. flax_model/alphafold3/model/atom_layout/atom_layout.py +1087 -0
  23. flax_model/alphafold3/model/components/haiku_modules.py +328 -0
  24. flax_model/alphafold3/model/components/mapping.py +240 -0
  25. flax_model/alphafold3/model/components/utils.py +75 -0
  26. flax_model/alphafold3/model/confidence_types.py +293 -0
  27. flax_model/alphafold3/model/confidences.py +655 -0
  28. flax_model/alphafold3/model/data3.py +117 -0
  29. flax_model/alphafold3/model/data_constants.py +19 -0
  30. flax_model/alphafold3/model/feat_batch.py +77 -0
  31. flax_model/alphafold3/model/features.py +2159 -0
  32. flax_model/alphafold3/model/mkdssp_pybind.cc +63 -0
  33. flax_model/alphafold3/model/mkdssp_pybind.h +26 -0
  34. flax_model/alphafold3/model/mmcif_metadata.py +222 -0
  35. flax_model/alphafold3/model/model.py +533 -0
  36. flax_model/alphafold3/model/model_config.py +24 -0
  37. flax_model/alphafold3/model/msa_pairing.py +304 -0
  38. flax_model/alphafold3/model/network/atom_cross_attention.py +413 -0
  39. flax_model/alphafold3/model/network/confidence_head.py +312 -0
  40. flax_model/alphafold3/model/network/diffusion_head.py +363 -0
  41. flax_model/alphafold3/model/network/diffusion_transformer.py +396 -0
  42. flax_model/alphafold3/model/network/distogram_head.py +75 -0
  43. flax_model/alphafold3/model/network/featurization.py +261 -0
  44. flax_model/alphafold3/model/network/noise_level_embeddings.py +133 -0
  45. flax_model/alphafold3/model/network/template_modules.py +343 -0
  46. flax_model/alphafold3/model/params.py +202 -0
  47. flax_model/alphafold3/model/pipeline/__init__.py +0 -0
  48. flax_model/alphafold3/model/pipeline/structure_cleaning.py +360 -0
  49. flax_model/alphafold3/model/protein_data_processing.py +118 -0
  50. flax_model/alphafold3/model/scoring/__init__.py +0 -0
flax_model/alphafold3/_data/libcifpp/mmcif_ddl.dic ADDED
The diff for this file is too large to render. See raw diff
 
flax_model/alphafold3/_data/libcifpp/mmcif_ma.dic ADDED
The diff for this file is too large to render. See raw diff
 
flax_model/alphafold3/_data/libcifpp/mmcif_pdbx.dic ADDED
The diff for this file is too large to render. See raw diff
 
flax_model/alphafold3/_tools/hmmer/.built ADDED
@@ -0,0 +1 @@
 
 
1
+ built
flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-compstruct.1 ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .TH "esl\-compstruct" 1 "Aug 2023" "Easel 0.49" "Easel Manual"
2
+
3
+ .SH NAME
4
+ esl\-compstruct \- calculate accuracy of RNA secondary structure predictions
5
+
6
+
7
+ .SH SYNOPSIS
8
+ .B esl\-compstruct
9
+ [\fIoptions\fR]
10
+ .I trusted_file
11
+ .I test_file
12
+
13
+
14
+
15
+ .SH DESCRIPTION
16
+
17
+ .PP
18
+ .B esl\-compstruct
19
+ evaluates the accuracy of RNA secondary structure predictions
20
+ on a per-base-pair basis.
21
+ The
22
+ .I trusted_file
23
+ contains one or more sequences with trusted (known) RNA
24
+ secondary structure annotation. The
25
+ .I test_file
26
+ contains the same sequences, in the same order, with
27
+ predicted RNA secondary structure annotation.
28
+ .B esl\-compstruct
29
+ reads the structures and compares them,
30
+ and calculates both the sensitivity (the number
31
+ of true base pairs that are correctly predicted)
32
+ and the positive predictive value (PPV;
33
+ the number of predicted base pairs that are true).
34
+ Results are reported for each individual sequence,
35
+ and in summary for all sequences together.
36
+
37
+ .PP
38
+ Both files must contain secondary structure annotation in
39
+ WUSS notation. Only SELEX and Stockholm formats support
40
+ structure markup at present.
41
+
42
+ .PP
43
+ The default definition of a correctly predicted base pair
44
+ is that a true pair (i,j) must exactly match a predicted
45
+ pair (i,j).
46
+
47
+ .PP
48
+ Mathews and colleagues (Mathews et al., JMB 288:911-940, 1999) use a
49
+ more relaxed definition. Mathews defines "correct" as follows: a true
50
+ pair (i,j) is correctly predicted if any of the following pairs are
51
+ predicted: (i,j), (i+1,j), (i\-1,j), (i,j+1), or (i,j\-1). This rule
52
+ allows for "slipped helices" off by one base. The
53
+ .B \-m
54
+ option activates this rule for both sensitivity and for
55
+ specificity. For specificity, the rule is reversed: predicted pair
56
+ (i,j) is considered to be true if the true structure contains one of
57
+ the five pairs (i,j), (i+1,j), (i\-1,j), (i,j+1), or (i,j\-1).
58
+
59
+
60
+
61
+
62
+ .SH OPTIONS
63
+
64
+ .TP
65
+ .B \-h
66
+ Print brief help; includes version number and summary of
67
+ all options, including expert options.
68
+
69
+ .TP
70
+ .B \-m
71
+ Use the Mathews relaxed accuracy rule (see above), instead
72
+ of requiring exact prediction of base pairs.
73
+
74
+ .TP
75
+ .B \-p
76
+ Count pseudoknotted base pairs towards the accuracy, in either trusted
77
+ or predicted structures. By default, pseudoknots are ignored.
78
+ .IP
79
+ Normally, only the
80
+ .I trusted_file
81
+ would have pseudoknot annotation, since most RNA secondary structure
82
+ prediction programs do not predict pseudoknots. Using the
83
+ .B \-p
84
+ option allows you to penalize the prediction program for not
85
+ predicting known pseudoknots. In a case where both the
86
+ .I trusted_file
87
+ and the
88
+ .I test_file
89
+ have pseudoknot annotation, the
90
+ .B \-p
91
+ option lets you count pseudoknots in evaluating
92
+ the prediction accuracy. Beware, however, the case where you
93
+ use a pseudoknot-capable prediction program to generate the
94
+ .IR test_file ,
95
+ but the
96
+ .I trusted_file
97
+ does not have pseudoknot annotation; in this case,
98
+ .B \-p
99
+ will penalize any predicted pseudoknots when it calculates
100
+ specificity, even if they're right, because they don't appear in the
101
+ trusted annotation. This is probably not what you'd want to do.
102
+
103
+
104
+
105
+ .SH EXPERT OPTIONS
106
+
107
+ .TP
108
+ .B \-\-quiet
109
+ Don't print any verbose header information. (Used by regression test
110
+ scripts, for example, to suppress version/date information.)
111
+
112
+
113
+
114
+ .SH SEE ALSO
115
+
116
+ .nf
117
+ http://bioeasel.org/
118
+ .fi
119
+
120
+ .SH COPYRIGHT
121
+
122
+ .nf
123
+ Copyright (C) 2023 Howard Hughes Medical Institute.
124
+ Freely distributed under the BSD open source license.
125
+ .fi
126
+
127
+ .SH AUTHOR
128
+
129
+ .nf
130
+ http://eddylab.org
131
+ .fi
132
+
133
+
134
+
flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-construct.1 ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .TH "esl\-construct" 1 "Aug 2023" "Easel 0.49" "Easel Manual"
2
+
3
+ .SH NAME
4
+ esl\-construct \- describe or create a consensus secondary structure
5
+
6
+ .SH SYNOPSIS
7
+ .B esl\-construct
8
+ [\fIoptions\fR]
9
+ .I msafile
10
+
11
+ .SH DESCRIPTION
12
+
13
+ .B esl\-construct
14
+ reports information on existing consensus secondary structure annotation
15
+ of an alignment or derives new consensus secondary structures based on
16
+ structure annotation for individual aligned sequences.
17
+
18
+ .PP
19
+ The alignment file must contain either individual sequence secondary
20
+ structure annotation (Stockholm #=GR SS), consensus secondary
21
+ structure annotation (Stockohlm #=GC SS_cons), or both. All structure
22
+ annotation must be in WUSS notation (Vienna dot paranetheses notation will be
23
+ correctly interpreted). At present, the alignment file must be in
24
+ Stockholm format and contain RNA or DNA sequences.
25
+
26
+ .PP
27
+ By default,
28
+ .B esl\-construct
29
+ generates lists the sequences in the alignment that have structure
30
+ annotation and the number of basepairs in those structures. If the
31
+ alignment also contains consensus structure annotation, the default output
32
+ will list how many of the individual basepairs overlap with the
33
+ consensus basepairs and how many conflict with a consensus basepair.
34
+
35
+ .PP
36
+ For the purposes of this miniapp, a basepair 'conflict' exists between
37
+ two basepairs in different structures, one between columns i and j and
38
+ the other between columns k and l, if (i == k and j != l) or (j == l
39
+ and i != k).
40
+
41
+ .PP
42
+ .B esl\-construct
43
+ can also be used to derive a new consensus structure based on
44
+ structure annotation for individual sequences in the alignment by
45
+ using any of the following options:
46
+ .BR \-x ,
47
+ .BR \-r ,
48
+ .BR \-c ,
49
+ \fB\-\-indi \fR\fI<s>\fR,
50
+ \fB\-\-ffreq \fR\fI<x>\fR,
51
+ .BR \-\-fmin .
52
+ These are described below. All of these options require the
53
+ .BI \-o " <f>"
54
+ option be used as well to specify that a new alignment file
55
+ .I <f>
56
+ be created. Differences between the new alignment(s) and the input
57
+ alignment(s) will be limited to the the consensus secondary structure (#=GC
58
+ SS_cons) annotation and possibly reference (#=GC RF) annotation.
59
+
60
+ .SH OPTIONS
61
+
62
+ .TP
63
+ .B \-h
64
+ Print brief help; includes version number and summary of
65
+ all options, including expert options.
66
+
67
+ .TP
68
+ .B \-a
69
+ List all alignment positions that are involved in at least one
70
+ conflicting basepair in at least one sequence to the screen, and then
71
+ exit.
72
+
73
+ .TP
74
+ .B \-v
75
+ Be verbose; with no other options, list individual sequence basepair
76
+ conflicts as well as summary statistics.
77
+
78
+ .TP
79
+ .B \-x
80
+ Compute a new consensus structure as the maximally sized set of
81
+ basepairs (greatest number of basepairs) chosen from all individual
82
+ structures that contains 0 conflicts. Output the alignment with the
83
+ new SS_cons annotation. This option must be used in combination with
84
+ the
85
+ .B \-o
86
+ option.
87
+
88
+ .TP
89
+ .B \-r
90
+ Remove any consensus basepairs that conflict with >= 1 individual
91
+ basepair and output the alignment with the new SS_cons annotation.
92
+ This option must be used in combination with the
93
+ .B \-o
94
+ option.
95
+
96
+ .TP
97
+ .B \-c
98
+ Define a new consensus secondary structure as the individual structure
99
+ annotation that has the maximum number of consistent basepairs with
100
+ the existing consensus secondary structure annotation.
101
+ This option must be used in combination with the
102
+ .B \-o
103
+ option.
104
+
105
+ .TP
106
+ .B \-\-rfc
107
+ With
108
+ .BR \-c ,
109
+ set the reference annotation (#=GC RF) as the sequence whose
110
+ individual structure becomes the consensus structure.
111
+
112
+ .TP
113
+ .BI \-\-indi " <s>"
114
+ Define a new consensus secondary structure as the individual structure
115
+ annotation from sequence named
116
+ .IR <s> .
117
+ This option must be used in combination with
118
+ the
119
+ .B \-o
120
+ option.
121
+
122
+ .TP
123
+ .B \-\-rfindi
124
+ With
125
+ \fB\-\-indi \fR\fI<s>\fR,
126
+ set the reference annotation (#=GC RF) as the sequence named
127
+ .BR <s> .
128
+
129
+ .TP
130
+ .BI \-\-ffreq " <x>"
131
+ Define a new consensus structure as the set of basepairs between
132
+ columns i:j that are paired in more than
133
+ .I <x>
134
+ fraction of the individual sequence structures.
135
+ This option must be used in combination with
136
+ the
137
+ .B \-o
138
+ option.
139
+
140
+ .TP
141
+ .B \-\-fmin
142
+ Same as
143
+ .BI \-\-ffreq " <x>"
144
+ except find the maximal
145
+ .I <x>
146
+ that gives a consistent consensus structure. A consistent structure
147
+ has each base (alignment position) as a member of at most 1 basepair.
148
+
149
+ .TP
150
+ .BI \-o " <s>",
151
+ Output the alignment(s) with new consensus structure annotation to file
152
+ .IR <f> .
153
+
154
+ .TP
155
+ .B \-\-pfam
156
+ With
157
+ .BR -o ,
158
+ specify that the alignment output format be Pfam format, a
159
+ special type of non-interleaved Stockholm on which each sequence
160
+ appears on a single line.
161
+
162
+ .TP
163
+ .BI \-l " <f>"
164
+ Create a new file
165
+ .I <f>
166
+ that lists the sequences that have at least one
167
+ basepair that conflicts with a consensus basepair.
168
+
169
+ .TP
170
+ .BI \-\-lmax " <n>"
171
+ With
172
+ .BR \-l ,
173
+ only list sequences that have more than
174
+ .I <n>
175
+ basepairs that conflict with the consensus structure to the list file.
176
+
177
+
178
+ .SH SEE ALSO
179
+
180
+ .nf
181
+ http://bioeasel.org/
182
+ .fi
183
+
184
+ .SH COPYRIGHT
185
+
186
+ .nf
187
+ Copyright (C) 2023 Howard Hughes Medical Institute.
188
+ Freely distributed under the BSD open source license.
189
+ .fi
190
+
191
+ .SH AUTHOR
192
+
193
+ .nf
194
+ http://eddylab.org
195
+ .fi
196
+
flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-seqrange.1 ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .TH "esl\-seqrange" 1 "Aug 2023" "Easel 0.49" "Easel Manual"
2
+
3
+ .SH NAME
4
+ esl\-seqrange \- determine a range of sequences for one of many parallel processes
5
+
6
+ .SH SYNOPSIS
7
+ .B esl\-sfetch
8
+ [\fIoptions\fR]
9
+ .I seqfile
10
+ .I procidx
11
+ .I nproc
12
+
13
+ .SH DESCRIPTION
14
+
15
+ .PP
16
+ .B esl\-seqrange
17
+ reads an SSI-indexed
18
+ .I seqfile
19
+ and determines the range of sequence indices in that file that process
20
+ number
21
+ .I procidx
22
+ out of
23
+ .I nproc
24
+ total processes should operate on during a parallel processing of
25
+ .IR seqfile .
26
+
27
+ .PP
28
+ The
29
+ .I seqfile
30
+ must be indexed first using
31
+ .B esl\-sfetch \-\-index
32
+ .IR seqfile .
33
+ This creates an SSI index file
34
+ .IR seqfile .ssi.
35
+ An SSI file is required in order for
36
+ .B esl\-seqrange
37
+ to work.
38
+
39
+ .PP
40
+ Sequence index ranges are calculated using a simple rule: the number
41
+ of sequences for each process should be identical, or as close as
42
+ possible to identical, across all processes. The lengths of the sequences
43
+ are not considered (even though they probably should be).
44
+
45
+ .SH OPTIONS
46
+
47
+ .TP
48
+ .B \-h
49
+ Print brief help; includes version number and summary of
50
+ all options, including expert options.
51
+
52
+ .TP
53
+ .BI \-\-informat " <s>"
54
+ Assert that input
55
+ .I seqfile
56
+ is in format
57
+ .IR <s> ,
58
+ bypassing format autodetection.
59
+ Common choices for
60
+ .I <s>
61
+ include:
62
+ .BR fasta ,
63
+ .BR embl ,
64
+ .BR genbank.
65
+ Alignment formats also work;
66
+ common choices include:
67
+ .BR stockholm ,
68
+ .BR a2m ,
69
+ .BR afa ,
70
+ .BR psiblast ,
71
+ .BR clustal ,
72
+ .BR phylip .
73
+ For more information, and for codes for some less common formats,
74
+ see main documentation.
75
+ The string
76
+ .I <s>
77
+ is case-insensitive (\fBfasta\fR or \fBFASTA\fR both work).
78
+
79
+
80
+
81
+ .SH SEE ALSO
82
+
83
+ .nf
84
+ http://bioeasel.org/
85
+ .fi
86
+
87
+ .SH COPYRIGHT
88
+
89
+ .nf
90
+ Copyright (C) 2023 Howard Hughes Medical Institute.
91
+ Freely distributed under the BSD open source license.
92
+ .fi
93
+
94
+ .SH AUTHOR
95
+
96
+ .nf
97
+ http://eddylab.org
98
+ .fi
99
+
flax_model/alphafold3/data/tools/hmmalign.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """A Python wrapper for hmmalign from the HMMER Suite."""
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ import os
7
+ import tempfile
8
+
9
+ from flax_model.alphafold3.data import parsers
10
+ from flax_model.alphafold3.data.tools import subprocess_utils
11
+
12
+
13
+ def _to_a3m(sequences: Sequence[str], name_prefix: str = 'sequence') -> str:
14
+ a3m = ''
15
+ for i, sequence in enumerate(sequences, 1):
16
+ a3m += f'> {name_prefix} {i}\n{sequence}\n'
17
+ return a3m
18
+
19
+
20
+ class Hmmalign:
21
+ """Python wrapper of the hmmalign binary."""
22
+
23
+ def __init__(self, binary_path: str):
24
+ """Initializes the Python hmmalign wrapper.
25
+
26
+ Args:
27
+ binary_path: Path to the hmmalign binary.
28
+
29
+ Raises:
30
+ RuntimeError: If hmmalign binary not found within the path.
31
+ """
32
+ self._binary_path = binary_path
33
+
34
+ subprocess_utils.check_binary_exists(path=self._binary_path, name='hmmalign')
35
+
36
+ def align_sequences(
37
+ self,
38
+ sequences: Sequence[str],
39
+ profile: str,
40
+ extra_flags: Mapping[str, str] | None = None,
41
+ ) -> str:
42
+ """Aligns sequence list to the profile and returns the alignment in A3M."""
43
+ return self.align(
44
+ a3m_str=_to_a3m(sequences, name_prefix='query'),
45
+ profile=profile,
46
+ extra_flags=extra_flags,
47
+ )
48
+
49
+ def align(
50
+ self,
51
+ a3m_str: str,
52
+ profile: str,
53
+ extra_flags: Mapping[str, str] | None = None,
54
+ ) -> str:
55
+ """Aligns sequences in A3M to the profile and returns the alignment in A3M.
56
+
57
+ Args:
58
+ a3m_str: A list of sequence strings.
59
+ profile: A hmm file with the hmm profile to align the sequences to.
60
+ extra_flags: Dictionary with extra flags, flag_name: flag_value, that are
61
+ added to hmmalign.
62
+
63
+ Returns:
64
+ An A3M string with the aligned sequences.
65
+
66
+ Raises:
67
+ RuntimeError: If hmmalign fails.
68
+ """
69
+ with tempfile.TemporaryDirectory() as query_tmp_dir:
70
+ input_profile = os.path.join(query_tmp_dir, 'profile.hmm')
71
+ input_sequences = os.path.join(query_tmp_dir, 'sequences.a3m')
72
+ output_a3m_path = os.path.join(query_tmp_dir, 'output.a3m')
73
+
74
+ with open(input_profile, 'w') as f:
75
+ f.write(profile)
76
+
77
+ with open(input_sequences, 'w') as f:
78
+ f.write(a3m_str)
79
+
80
+ cmd = [
81
+ self._binary_path,
82
+ *('-o', output_a3m_path),
83
+ *('--outformat', 'A2M'), # A2M is A3M in the HMMER suite.
84
+ ]
85
+ if extra_flags:
86
+ for flag_name, flag_value in extra_flags.items():
87
+ cmd.extend([flag_name, flag_value])
88
+ cmd.extend([input_profile, input_sequences])
89
+
90
+ subprocess_utils.run(
91
+ cmd=cmd,
92
+ cmd_name='hmmalign',
93
+ log_stdout=False,
94
+ log_stderr=True,
95
+ log_on_process_error=True,
96
+ )
97
+
98
+ with open(output_a3m_path, encoding='utf-8') as f:
99
+ a3m = f.read()
100
+
101
+ return a3m
102
+
103
+ def align_sequences_to_profile(self, profile: str, sequences_a3m: str) -> str:
104
+ """Aligns the sequences to profile and returns the alignment in A3M string.
105
+
106
+ Uses hmmalign to align the sequences to the profile, then ouputs the
107
+ sequence contatenated at the beginning of the sequences in the A3M format.
108
+ As the sequences are represented by an alignment with possible gaps ('-')
109
+ and insertions (lowercase characters), the method first removes the gaps,
110
+ then uppercases the insertions to prepare the sequences for realignment.
111
+ Sequences with gaps cannot be aligned, as '-'s are not a valid symbol to
112
+ align; lowercase characters must be uppercased to preserve the original
113
+ sequences before realignment.
114
+
115
+ Args:
116
+ profile: The Hmmbuild profile to align the sequences to.
117
+ sequences_a3m: Sequences in A3M format to align to the profile.
118
+
119
+ Returns:
120
+ An A3M string with the aligned sequences.
121
+
122
+ Raises:
123
+ RuntimeError: If hmmalign fails.
124
+ """
125
+ deletion_table = str.maketrans('', '', '-')
126
+ sequences_no_gaps_a3m = []
127
+ for seq, desc in parsers.lazy_parse_fasta_string(sequences_a3m):
128
+ sequences_no_gaps_a3m.append(f'>{desc}')
129
+ sequences_no_gaps_a3m.append(seq.translate(deletion_table))
130
+ sequences_no_gaps_a3m = '\n'.join(sequences_no_gaps_a3m)
131
+
132
+ aligned_sequences = self.align(sequences_no_gaps_a3m, profile)
133
+
134
+ return aligned_sequences
flax_model/alphafold3/jax/attention/attention.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Scaled dot-product attention."""
4
+
5
+ import typing
6
+ from typing import Literal, TypeAlias
7
+
8
+ from flax_model.alphafold3.jax.attention import attention_base as base
9
+ from flax_model.alphafold3.jax.attention import flash_attention as attention_triton
10
+ from flax_model.alphafold3.jax.attention import xla_attention
11
+ from flax_model.alphafold3.jax.common import triton_utils
12
+ import jax
13
+ from jax.typing import DTypeLike # pylint: disable=g-importing-member
14
+ import jaxtyping
15
+ from jaxtyping import Array # pylint: disable=g-importing-member
16
+ from jaxtyping import Bool # pylint: disable=g-importing-member
17
+ from jaxtyping import Float # pylint: disable=g-importing-member
18
+ import typeguard
19
+
20
+ Implementation: TypeAlias = Literal["cudnn", "xla", "triton", "cutlass"]
21
+
22
+
23
+ @jaxtyping.jaxtyped(typechecker=typeguard.typechecked)
24
+ def dot_product_attention(
25
+ query: Float[Array, "*B T H D"],
26
+ key: Float[Array, "*B t #H D"],
27
+ value: Float[Array, "*B t #H D"],
28
+ *,
29
+ bias: Float[Array, "*#B #H #T #t"] | None = None,
30
+ mask: Bool[Array, "*#B #H #T #t"] | None = None,
31
+ implementation: Implementation | None = None,
32
+ logits_dtype: DTypeLike | None = None,
33
+ precision: (
34
+ jax.lax.Precision | tuple[jax.lax.Precision, jax.lax.Precision] | None
35
+ ) = None,
36
+ ) -> Float[Array, "*B T H D"]:
37
+ """Performs scaled dot-product attention.
38
+
39
+ Scaled dot-product attention from "Attention is all you need"
40
+ https://arxiv.org/abs/1706.03762.
41
+
42
+ Computes self- or cross-attention. The following is computed:
43
+ softmax(qk_scale * query @ key^T + bias) @ value.
44
+
45
+ Supports both multi-head and multi-query attention
46
+ (https://arxiv.org/abs/1911.02150).
47
+
48
+ Arguments:
49
+ query: Query array of shape `[batch, seq_len_q, num_heads, head_dim]`.
50
+ key: Key array of shape `[batch, seq_len_kv, num_heads, head_dim]`.
51
+ `num_heads` can be 1 for multi-query attention.
52
+ value: Value array of shape `[batch, seq_len_kv, num_heads, head_dim]`.
53
+ `num_heads` can be 1 for multi-query attention.
54
+ bias: Optional bias array, broadcastable to shape `[batch, num_heads,
55
+ seq_len_q, seq_len_kv]`.
56
+ mask: Optional boolean mask, broadcastable to `[batch, num_heads, seq_len_q,
57
+ seq_len_kv]`. Attention weights are masked out if the corresponding mask
58
+ value is `False`.
59
+ implementation: if `None` (default), an implementation is automatically
60
+ chosen. 'xla' will use standard XLA and work on any platform, 'triton'
61
+ will use a fused Triton GPU kernel, and 'cudnn' a cuDNN FlashAttention
62
+ kernel. Only a subset of data types, shapes and GPUs are supported by
63
+ 'triton' and 'cudnn', with an exception thrown in this case.
64
+ logits_dtype: Data type for attention logits (`query @ key^T`). If `None` is
65
+ passed (the default), the accumulator type from the `query @ key^T` dot
66
+ product will be used, which is FP32 for BF16/FP16/FP32 inputs. Note that
67
+ this default increases the memory usage for BF16/FP16 inputs when using
68
+ `implementation='xla'`, but does not increase memory usage when using
69
+ `implementation='triton'`.
70
+ precision: The precision for the dot products. Either `None` (default) which
71
+ uses the default JAX precision for a backend; a tuple `(
72
+ query_key_dot_precision, weights_value_dot_precision)` of
73
+ `jax.lax.Precision` objects; or a single `jax.lax.Precision` object
74
+ applied to both dot products.
75
+
76
+ Returns:
77
+ An array with the same shape as `query`.
78
+ """
79
+
80
+ if implementation is not None:
81
+ named_args = typing.get_args(Implementation)
82
+ if implementation not in named_args:
83
+ raise ValueError(
84
+ f"Unsupported named implementation. Must be one of {named_args}."
85
+ )
86
+
87
+ if implementation == "cudnn":
88
+ if logits_dtype is not None:
89
+ raise ValueError(
90
+ "logits_dtype is not supported for cudnn implementation."
91
+ )
92
+ if precision is not None:
93
+ raise NotImplementedError(
94
+ "precision is not supported for cudnn implementation."
95
+ )
96
+
97
+ return jax.nn.dot_product_attention(
98
+ query=query,
99
+ key=key,
100
+ value=value,
101
+ bias=bias,
102
+ mask=mask,
103
+ implementation="cudnn",
104
+ )
105
+
106
+ if implementation == "cutlass":
107
+ if logits_dtype is not None:
108
+ raise ValueError(
109
+ "logits_dtype is not supported for cutlass implementation."
110
+ )
111
+ if precision is not None:
112
+ raise NotImplementedError(
113
+ "precision is not supported for cutlass implementation."
114
+ )
115
+
116
+ return jax.nn.dot_product_attention(
117
+ query=query,
118
+ key=key,
119
+ value=value,
120
+ bias=bias,
121
+ mask=mask,
122
+ implementation="cutlass",
123
+ )
124
+
125
+ logits_dtype = base.AUTO if logits_dtype is None else logits_dtype
126
+ precision = jax.lax.Precision.DEFAULT if precision is None else precision
127
+
128
+ args = (query, key, value)
129
+ kwargs = dict(
130
+ precision=precision,
131
+ logits_dtype=logits_dtype,
132
+ bias=bias,
133
+ mask=mask,
134
+ )
135
+
136
+ if implementation == "triton":
137
+ if not triton_utils.has_triton_support():
138
+ raise ValueError(
139
+ "implementation='triton' for FlashAttention is unsupported on this"
140
+ " GPU generation. Please use implementation='xla' instead."
141
+ )
142
+ return attention_triton.TritonFlashAttention()(*args, **kwargs)
143
+
144
+ if implementation is None and triton_utils.has_triton_support():
145
+ try:
146
+ return attention_triton.TritonFlashAttention()(*args, **kwargs)
147
+ except Exception: # pylint: disable=broad-exception-caught
148
+ pass # Fallback to XLA.
149
+
150
+ return xla_attention.XlaDotProductAttention()(*args, **kwargs)
flax_model/alphafold3/jax/common/array_view.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Array view class and utilities."""
4
+
5
+ from collections.abc import Sequence
6
+ import dataclasses
7
+ import math
8
+ import operator
9
+ from types import EllipsisType # pylint: disable=g-importing-member
10
+ from typing import Any, Self, TypeAlias, TypeVar
11
+
12
+ import jax
13
+ import jax.experimental
14
+ from jax.experimental import pallas as pl
15
+ import jax.numpy as jnp
16
+ from jax.typing import ArrayLike # pylint: disable=g-importing-member
17
+ from jaxtyping import Int # pylint: disable=g-importing-member
18
+ import numpy as np
19
+
20
+ ArrayT: TypeAlias = Any
21
+ ScalarInt: TypeAlias = (
22
+ Int[ArrayT, ""] | Int[np.generic, ""] | Int[jnp.generic, ""]
23
+ )
24
+
25
+ Indexer: TypeAlias = int | ScalarInt | slice | pl.Slice | EllipsisType
26
+
27
+
28
+ @jax.tree_util.register_pytree_node_class
29
+ @dataclasses.dataclass(frozen=True)
30
+ class ArrayView:
31
+ """A strided view of a JAX array."""
32
+
33
+ base: jax.Array
34
+ _: dataclasses.KW_ONLY
35
+ # These are set by `__post_init__` so `None` value is never seen after init.
36
+ shape: tuple[int, ...] = None # type: ignore
37
+ strides: tuple[int, ...] = None # type: ignore
38
+ offset: int | ScalarInt = 0
39
+ flatten_base: bool = True
40
+
41
+ def __post_init__(self):
42
+ if self.shape is None:
43
+ object.__setattr__(self, "shape", self.base.shape)
44
+
45
+ if self.strides is None:
46
+ object.__setattr__(self, "strides", pl.strides_from_shape(self.shape))
47
+
48
+ if len(self.shape) != len(self.strides):
49
+ raise ValueError("`shape` and `strides` must have the same length.")
50
+
51
+ # Within `jax.vjp`, we can get non-`Array` values here (such as `object`).
52
+ if isinstance(self.base, jax.Array):
53
+ if isinstance(self.offset, int):
54
+ if not (0 <= self.offset < max(self.base.size, 1)):
55
+ raise ValueError("Invalid `offset`.")
56
+
57
+ if self.flatten_base:
58
+ if len(self.base.shape) != 1:
59
+ object.__setattr__(self, "base", self.base.reshape((-1,)))
60
+
61
+ def tree_flatten(self):
62
+ if isinstance(self.offset, int):
63
+ return (self.base,), (self.offset, self.shape, self.strides)
64
+ return (self.base, self.offset), (self.shape, self.strides)
65
+
66
+ @classmethod
67
+ def tree_unflatten(cls, aux, children) -> Self:
68
+ base, offset, shape, strides = (*children, *aux)
69
+ return cls(base, shape=shape, strides=strides, offset=offset)
70
+
71
+ @property
72
+ def dtype(self) -> jnp.dtype:
73
+ return self.base.dtype
74
+
75
+ @property
76
+ def size(self) -> int:
77
+ return math.prod(self.shape)
78
+
79
+ @property
80
+ def ndim(self) -> int:
81
+ return len(self.shape)
82
+
83
+ @property
84
+ def T(self) -> Self: # pylint: disable=invalid-name
85
+ return self.transpose()
86
+
87
+ @property
88
+ def _index_dtype(self) -> jax.typing.DTypeLike:
89
+ i32_max = jnp.iinfo(jnp.int32).max
90
+ return jnp.int32 if (self.base.size <= i32_max) else jnp.int64
91
+
92
+ @property
93
+ def offsets(self) -> jax.Array:
94
+ """Returns array of offsets into `base` for each element."""
95
+ with jax.experimental.enable_x64():
96
+ idxs = jnp.indices(self.shape, sparse=True, dtype=self._index_dtype)
97
+ return self.offset + sum(s * idx for s, idx in zip(self.strides, idxs))
98
+
99
+ def astype(self, dtype: jax.typing.DTypeLike) -> Self:
100
+ return self._replace(base=self.base.astype(dtype))
101
+
102
+ def broadcast_to_rank(self, rank: int) -> Self:
103
+ """Returns a new view with the specified rank."""
104
+ if rank < self.ndim:
105
+ raise ValueError(f"Cannot broadcast to lower rank: {rank} < {self.ndim}.")
106
+
107
+ shape = (1,) * (rank - self.ndim) + self.shape
108
+ strides = (0,) * (rank - self.ndim) + self.strides
109
+ return self._replace(shape=shape, strides=strides)
110
+
111
+ def broadcast_to(self, shape: tuple[int, ...]) -> Self:
112
+ """Returns a new view with the specified shape."""
113
+ view = self.broadcast_to_rank(len(shape))
114
+ strides = []
115
+ for dim_size, stride, target_size in zip(
116
+ view.shape, view.strides, shape, strict=True
117
+ ):
118
+ if dim_size == target_size:
119
+ strides.append(stride)
120
+ elif dim_size == 1:
121
+ strides.append(0)
122
+ else:
123
+ raise ValueError(f"Cannot broadcast {self.shape} to {shape}.")
124
+ return self._replace(shape=shape, strides=strides)
125
+
126
+ def collapse(
127
+ self, start: int, stop: int | None = None, *, allow_copy: bool = False
128
+ ) -> Self:
129
+ """Returns a new view with the axis range collapsed into one axis."""
130
+ lo, hi, _ = slice(start, stop).indices(self.ndim)
131
+ if hi < lo:
132
+ raise ValueError(
133
+ "Invalid dimension range passed to collapse: "
134
+ f"{self.shape} [{start}:{stop}]"
135
+ )
136
+ shape = self.shape[:lo] + (-1,) + self.shape[hi:]
137
+ return self.reshape(shape, allow_copy=allow_copy)
138
+
139
+ def reshape(self, shape: Sequence[int], *, allow_copy: bool = False) -> Self:
140
+ """Returns a new view with the specified shape."""
141
+ try:
142
+ return self._reshape(tuple(shape))
143
+ except ValueError:
144
+ if not allow_copy:
145
+ raise
146
+ return type(self)(jnp.array(self)).reshape(shape)
147
+
148
+ def _reshape(self, shape: tuple[int, ...]) -> Self:
149
+ """Returns a new view with the specified shape."""
150
+
151
+ if (num_minus_one_dims := shape.count(-1)) > 0:
152
+ if num_minus_one_dims > 1:
153
+ raise ValueError("`shape` may only contain a single `-1` dimension.")
154
+ pos = shape.index(-1)
155
+ shape = list(shape)
156
+ shape[pos] = self.size // math.prod(d for d in shape if d != -1)
157
+
158
+ if math.prod(shape) != self.size:
159
+ raise ValueError("Mismatched number of elements.")
160
+
161
+ # Logic copied from `numpy` C++ code.
162
+ # Remove axes with length 1, to simplify logic below.
163
+ old_shape = [d for d in self.shape if d != 1]
164
+ old_strides = [s for i, s in enumerate(self.strides) if self.shape[i] != 1]
165
+ strides = [0] * len(shape)
166
+
167
+ # Axes currently being worked upon.
168
+ old_start, old_stop = 0, 1
169
+ new_start, new_stop = 0, 1
170
+
171
+ while (old_start < len(old_shape)) and (new_start < len(shape)):
172
+ old_axes_prod = old_shape[old_start]
173
+ new_axes_prod = shape[new_start]
174
+ while old_axes_prod != new_axes_prod:
175
+ if old_axes_prod < new_axes_prod:
176
+ old_axes_prod *= old_shape[old_stop]
177
+ old_stop += 1
178
+ else:
179
+ new_axes_prod *= shape[new_stop]
180
+ new_stop += 1
181
+
182
+ # Check if original axes can be combined.
183
+ for i in range(old_start, old_stop - 1):
184
+ if old_strides[i] != old_shape[i + 1] * old_strides[i + 1]:
185
+ raise ValueError("Cannot combine axes non-contiguous in memory.")
186
+
187
+ # Calculate new strides.
188
+ strides[new_stop - 1] = old_strides[old_stop - 1]
189
+ for i in range(new_stop - 1, new_start, -1):
190
+ strides[i - 1] = strides[i] * shape[i]
191
+
192
+ old_start, old_stop = old_stop, old_stop + 1
193
+ new_start, new_stop = new_stop, new_stop + 1
194
+
195
+ return self._replace(shape=shape, strides=strides)
196
+
197
+ def split(
198
+ self, indices_or_sections: int | Sequence[int], axis: int = 0
199
+ ) -> tuple[Self, ...]:
200
+ """Splits the view into multiple slice views."""
201
+ if isinstance(indices_or_sections, int):
202
+ if self.shape[axis] % indices_or_sections != 0:
203
+ raise ValueError("Axis size is not divisible by number of sections.")
204
+
205
+ chunk = self.shape[axis] // indices_or_sections
206
+ indices_or_sections = [i * chunk for i in range(1, indices_or_sections)]
207
+
208
+ los = (0, *indices_or_sections)
209
+ his = (*indices_or_sections, None)
210
+ slice_prefix = (slice(None),) * _canonicalize_axis(axis, self.ndim)
211
+ return tuple(self[*slice_prefix, slice(lo, hi)] for lo, hi in zip(los, his))
212
+
213
+ def swapaxes(self, axis1: int, axis2: int) -> Self:
214
+ """Returns a new view with the specified axis swapped."""
215
+ axes = list(range(self.ndim))
216
+ axes[axis1], axes[axis2] = axes[axis2], axes[axis1]
217
+ return self.transpose(axes)
218
+
219
+ def moveaxis(self, source: int, destination: int) -> Self:
220
+ """Returns a new view with the specified axis moved."""
221
+ source, destination = source % self.ndim, destination % self.ndim
222
+ axes = list(range(self.ndim))
223
+ del axes[source]
224
+ axes.insert(destination, source)
225
+ return self.transpose(axes)
226
+
227
+ def transpose(self, axes: Sequence[int] | None = None) -> Self:
228
+ """Returns a new view with the specified axes order."""
229
+ if axes is None:
230
+ axes = tuple(reversed(range(self.ndim)))
231
+ if len(axes) != self.ndim:
232
+ raise ValueError("`axes` must have the same dimensionality as the array.")
233
+ shape = tuple(self.shape[a] for a in axes)
234
+ strides = tuple(self.strides[a] for a in axes)
235
+ return self._replace(shape=shape, strides=strides)
236
+
237
+ def __getitem__(self, idxs: Indexer | tuple[Indexer, ...]) -> Self:
238
+ if not isinstance(idxs, tuple):
239
+ idxs = (idxs,)
240
+
241
+ if len(idxs) > self.ndim:
242
+ raise ValueError("Too many slice indices.")
243
+
244
+ num_ellipses = idxs.count(Ellipsis)
245
+ if num_ellipses > 1:
246
+ raise ValueError("Multiple `...` are not supported.")
247
+ elif num_ellipses == 0:
248
+ idxs += (Ellipsis,) # `[a:b]` is equivalent to `[a:b, ...]`.
249
+
250
+ # Replace `...` with slices that take the entirety of the missing axes.
251
+ ellipsis_idx = idxs.index(Ellipsis)
252
+ ellipsis_slices = (slice(None),) * (self.ndim - len(idxs) + 1)
253
+ idxs = idxs[:ellipsis_idx] + ellipsis_slices + idxs[ellipsis_idx + 1 :]
254
+
255
+ shape = []
256
+ strides = []
257
+ with jax.experimental.enable_x64():
258
+
259
+ def as_index(x):
260
+ return x.astype(self._index_dtype) if isinstance(x, jax.Array) else x
261
+
262
+ offset = as_index(self.offset)
263
+
264
+ for idx, dim, stride in zip(idxs, self.shape, self.strides, strict=True):
265
+ if isinstance(idx, int):
266
+ if not (-dim <= idx < dim):
267
+ raise ValueError("Slice index out of range.")
268
+ offset += stride * (idx % dim)
269
+ elif isinstance(idx, ScalarInt):
270
+ offset += stride * as_index(idx)
271
+ elif isinstance(idx, slice):
272
+ start, stop, step = idx.indices(dim)
273
+ if step >= 0:
274
+ shape.append(pl.cdiv(stop - start, step))
275
+ else:
276
+ shape.append(pl.cdiv(start - stop, -step))
277
+ strides.append(stride * step)
278
+ offset += stride * start
279
+ elif isinstance(idx, pl.Slice):
280
+ shape.append(idx.size)
281
+ strides.append(stride * idx.stride)
282
+ offset += stride * as_index(idx.start)
283
+ else:
284
+ raise ValueError(f"Unexpected indexer: {idx}")
285
+
286
+ return self._replace(shape=shape, strides=strides, offset=offset)
287
+
288
+ def _replace(self, **kwargs) -> Self:
289
+ if "shape" in kwargs:
290
+ kwargs["shape"] = tuple(kwargs["shape"])
291
+ if "strides" in kwargs:
292
+ kwargs["strides"] = tuple(kwargs["strides"])
293
+ return dataclasses.replace(self, **kwargs)
294
+
295
+ def set(self, value: ArrayLike | "ArrayView") -> Self:
296
+ """Returns a new view with the views values set to `value`."""
297
+ if any(s == 0 for s in self.strides):
298
+ raise ValueError("Cannot set values on a broadcasted array.")
299
+
300
+ # Try to just transpose the value, if possible.
301
+ major_to_minor = np.argsort(-np.array(self.strides), kind="stable")
302
+ value = jnp.array(value)
303
+ value_transposed = value.transpose(major_to_minor)
304
+ if (
305
+ self.transpose(major_to_minor).strides
306
+ == ArrayView(value_transposed).strides
307
+ ):
308
+ base = jax.lax.dynamic_update_slice(
309
+ self.base, value_transposed.flatten(), (self.offset,)
310
+ )
311
+ else:
312
+ base = self.base.at[self.offsets].set(value)
313
+ return self._replace(base=base)
314
+
315
+ def __jax_array__(self) -> jax.Array:
316
+ """Returns values as a dense array."""
317
+ # Try to express using transpose, slice, and reshape, to encourage XLA to
318
+ # fuse into other ops, rather than materialising the values. Otherwise,
319
+ # fall back to using a gather.
320
+ if (self.ndim == 0) or any(s < 0 for s in self.strides):
321
+ return self.base[self.offsets]
322
+
323
+ major_to_minor = np.argsort(-np.array(self.strides), kind="stable")
324
+
325
+ # Construct a shape that gives us the correct strides.
326
+ bcast_axes = []
327
+ shape = []
328
+ for axis in major_to_minor[::-1]: # minor to major
329
+ stride = self.strides[axis]
330
+ if stride == 0:
331
+ bcast_axes.append(axis)
332
+ shape.append(1)
333
+ continue
334
+
335
+ if stride % math.prod(shape) != 0:
336
+ raise ValueError("Cannot express as a reshape, then slice.")
337
+ shape.append(stride // math.prod(shape))
338
+
339
+ if self.base.size % math.prod(shape) != 0:
340
+ return self.base[self.offsets]
341
+
342
+ shape = [self.base.size // math.prod(shape), *reversed(shape)]
343
+ slice_sizes = [
344
+ *(1 if a in bcast_axes else self.shape[a] for a in major_to_minor),
345
+ 1,
346
+ ]
347
+
348
+ if shape[0] == self.shape[major_to_minor[0]]:
349
+ needs_offset_slice = False
350
+ elif not isinstance(self.offset, int):
351
+ needs_offset_slice = True
352
+ else:
353
+ start_indices = np.unravel_index(self.offset, shape)
354
+ end_indices = [s + size for s, size in zip(start_indices, slice_sizes)]
355
+ needs_offset_slice = any(e > dim for e, dim in zip(end_indices, shape))
356
+
357
+ if needs_offset_slice:
358
+ shape[0] = self.shape[major_to_minor[0]]
359
+ size = math.prod(shape)
360
+ # The pad is necessary to ensure that the dynamic slice is in range.
361
+ vals = jnp.pad(self.base, (0, size))
362
+ vals = jax.lax.dynamic_slice(vals, (self.offset,), (size,))
363
+ start_indices = [0] * len(shape)
364
+ else:
365
+ vals = self.base
366
+ start_indices = jnp.unravel_index(self.offset, shape)
367
+
368
+ vals = vals.reshape(shape)
369
+ vals = jax.lax.dynamic_slice(vals, start_indices, slice_sizes)[..., 0]
370
+ # Move axes from their physical ordering to their logical ordering.
371
+ vals = vals.transpose(np.argsort(major_to_minor))
372
+ return jnp.broadcast_to(vals, self.shape)
373
+
374
+
375
+ def as_array_view(x: jax.Array | ArrayView) -> ArrayView:
376
+ return x if isinstance(x, ArrayView) else ArrayView(x)
377
+
378
+
379
+ T = TypeVar("T", jax.Array, ArrayView)
380
+
381
+
382
+ def zeros_like(x: T) -> T:
383
+ if isinstance(x, ArrayView):
384
+ return x._replace(base=jnp.zeros_like(x.base))
385
+ return jnp.zeros_like(x)
386
+
387
+
388
+ def _canonicalize_axis(axis, num_dims) -> int:
389
+ """Canonicalize an axis in [-num_dims, num_dims) to [0, num_dims)."""
390
+ axis = operator.index(axis)
391
+ if not -num_dims <= axis < num_dims:
392
+ raise ValueError(
393
+ f"axis {axis} is out of bounds for array of dimension {num_dims}"
394
+ )
395
+ if axis < 0:
396
+ axis = axis + num_dims
397
+ return axis
flax_model/alphafold3/jax/common/precision.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Precision classes and utilities."""
4
+
5
+ import enum
6
+
7
+ import jax
8
+ import jax.numpy as jnp
9
+
10
+
11
+ @enum.unique
12
+ class DotPrecision(enum.Enum):
13
+ """Precision for `dot` operation.
14
+
15
+ Naming scheme: {OPERAND_DTYPE}_{ACCUMULATOR_DTYPE}[_{NUM_PASSES}x]
16
+ """
17
+
18
+ BF16_F32 = "bf16_f32"
19
+
20
+ # GPU only precisions.
21
+ F32_F32 = "f32_f32" # Full f32 precision (doesn't use TensorCores).
22
+ TF32_F32 = "tf32_f32" # Equivalent to `DEFAULT`/`HIGH` on GPU.
23
+ TF32_F32_3X = "tf32_f32_3x"
24
+ F16_F16 = "f16_f16"
25
+ F16_F32 = "f16_f32"
26
+
27
+ @property
28
+ def operand_dtype(self) -> jnp.dtype:
29
+ match self:
30
+ case DotPrecision.BF16_F32:
31
+ return jnp.bfloat16
32
+ case DotPrecision.F16_F16 | DotPrecision.F16_F32:
33
+ return jnp.float16
34
+ case _:
35
+ return jnp.float32
36
+
37
+ @property
38
+ def accumulator_dtype(self) -> jnp.dtype:
39
+ return jnp.float16 if (self == DotPrecision.F16_F16) else jnp.float32
40
+
41
+
42
+ _JAX_GPU_PRECISION_MAP = {
43
+ (jnp.float16, jax.lax.Precision.DEFAULT): DotPrecision.F16_F32,
44
+ (jnp.bfloat16, jax.lax.Precision.DEFAULT): DotPrecision.BF16_F32,
45
+ (jnp.float32, jax.lax.Precision.DEFAULT): DotPrecision.TF32_F32,
46
+ (jnp.float32, jax.lax.Precision.HIGH): DotPrecision.TF32_F32,
47
+ (jnp.float32, jax.lax.Precision.HIGHEST): DotPrecision.F32_F32,
48
+ }
49
+
50
+ _JAX_CPU_PRECISION_MAP = {
51
+ (jnp.float16, jax.lax.Precision.DEFAULT): DotPrecision.F16_F32,
52
+ (jnp.bfloat16, jax.lax.Precision.DEFAULT): DotPrecision.F32_F32,
53
+ (jnp.float32, jax.lax.Precision.DEFAULT): DotPrecision.F32_F32,
54
+ (jnp.float32, jax.lax.Precision.HIGH): DotPrecision.F32_F32,
55
+ (jnp.float32, jax.lax.Precision.HIGHEST): DotPrecision.F32_F32,
56
+ }
57
+
58
+
59
+ def _create_jax_precision_map():
60
+ precision_map = {}
61
+ for (dtype, jax_precision), dot_precision in _JAX_GPU_PRECISION_MAP.items():
62
+ precision_map[("gpu", jnp.dtype(dtype), jax_precision)] = dot_precision
63
+ for (dtype, jax_precision), dot_precision in _JAX_CPU_PRECISION_MAP.items():
64
+ precision_map[("cpu", jnp.dtype(dtype), jax_precision)] = dot_precision
65
+ return precision_map
66
+
67
+
68
+ _JAX_PRECISION_MAP = _create_jax_precision_map()
69
+
70
+
71
+ def get_equivalent_dot_precision(
72
+ a_dtype: jnp.dtype, b_dtype: jnp.dtype, jax_precision: jax.lax.Precision
73
+ ) -> DotPrecision:
74
+ """Returns `DotPrecision` replicating default XLA behaviour."""
75
+ if a_dtype != b_dtype:
76
+ raise ValueError("Cannot infer precision if operand types differ.")
77
+
78
+ backend = jax.default_backend().lower()
79
+ if (jax_precision != jax.lax.Precision.DEFAULT) and (a_dtype != jnp.float32):
80
+ raise ValueError(
81
+ "`jax.lax.Precision` values other than `DEFAULT` only have an effect if"
82
+ " the operand type is `float32`."
83
+ )
84
+ return _JAX_PRECISION_MAP[(backend, a_dtype, jax_precision)]
flax_model/alphafold3/jax/gated_linear_unit/__init__.py ADDED
File without changes
flax_model/alphafold3/jax/gated_linear_unit/gated_linear_unit_base.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Common types for gated linear unit kernels."""
4
+
5
+ import abc
6
+ from collections.abc import Callable
7
+ import functools
8
+ from typing import Any
9
+
10
+ import jax
11
+ import jax.numpy as jnp
12
+ import jaxtyping
13
+ from jaxtyping import Array, Float # pylint: disable=g-importing-member,g-multiple-import
14
+ import typeguard
15
+
16
+
17
+ class GatedLinearUnit(abc.ABC):
18
+ """Gated linear unit."""
19
+
20
+ def __call__(
21
+ self,
22
+ x: Float[Array, '*B M K'],
23
+ weight: Float[Array, 'K 2 N'],
24
+ *,
25
+ activation: Callable[[jax.Array], jax.Array] | None = None,
26
+ precision: jax.lax.Precision | None = None,
27
+ **kwargs,
28
+ ) -> Float[Array, '*B M N']:
29
+ """Applies a gated linear unit (https://arxiv.org/abs/1612.08083).
30
+
31
+ Computes `activation(x @ weight[:, 0]) * x @ weight[:, 1]`.
32
+
33
+ Args:
34
+ x: the input array.
35
+ weight: the combined weight array.
36
+ activation: optional activation function.
37
+ precision: specifies the matrix multiplication precision. Either `None`
38
+ (default), which means the default precision for the backend, or a
39
+ `jax.lax.Precision` enum.
40
+ **kwargs: additional keyword arguments.
41
+
42
+ Returns:
43
+ The output array.
44
+ """
45
+ return self._fwd(
46
+ x, weight, activation=activation, precision=precision, **kwargs
47
+ )
48
+
49
+ # Default vmap rule.
50
+ @property
51
+ def vmap_rule_forward(self) -> Callable[..., Any]:
52
+ def _vmap_rule(
53
+ axis_size, in_batched, *args, fn: jax.custom_batching.custom_vmap
54
+ ):
55
+ sequential_vmap = jax.custom_batching.sequential_vmap(fn.fun)
56
+ return sequential_vmap.vmap_rule(axis_size, in_batched, *args)
57
+
58
+ return _vmap_rule
59
+
60
+ def apply_vmap_rule_forward(
61
+ self, fn: Callable[..., Any], **kwargs
62
+ ) -> jax.custom_batching.custom_vmap:
63
+ fn_closed = functools.partial(fn, **kwargs)
64
+ fn_closed = jax.custom_batching.custom_vmap(fn_closed)
65
+ vmap_rule = functools.partial(self.vmap_rule_forward, fn=fn_closed)
66
+ fn_closed.def_vmap(vmap_rule)
67
+ return fn_closed
68
+
69
+ @abc.abstractmethod
70
+ def _fwd(
71
+ self,
72
+ x: Float[Array, '*B M K'],
73
+ weight: Float[Array, 'K 2 N'],
74
+ *,
75
+ activation: Callable[[jax.Array], jax.Array] | None,
76
+ precision: jax.lax.Precision | None,
77
+ ) -> Float[Array, '*B M N']:
78
+ """Gated linear unit."""
79
+ ...
80
+
81
+
82
+ @jaxtyping.jaxtyped(typechecker=typeguard.typechecked)
83
+ def gated_linear_unit_xla(
84
+ x: Float[Array, '*B M K'],
85
+ weight: Float[Array, 'K 2 N'],
86
+ *,
87
+ activation: Callable[[jax.Array], jax.Array] | None = None,
88
+ precision: jax.lax.Precision | None = None,
89
+ ) -> Float[Array, '*B M N']:
90
+ """Applies a gated linear unit (https://arxiv.org/abs/1612.08083).
91
+
92
+ Computes `activation(x @ weight[:, 0]) * x @ weight[:, 1]`.
93
+
94
+ This is SwiGLU when `activation=jax.nn.swish`, GEGLU when
95
+ `activation=jax.nn.gelu`, REGLU when `activation=jax.nn.relu`, and GLU when
96
+ `activation=jax.nn.sigmoid` (https://arxiv.org/abs/2002.05202).
97
+
98
+ Args:
99
+ x: the input array.
100
+ weight: the combined weight array.
101
+ activation: optional activation function.
102
+ precision: specifies the matrix multiplication precision. Either `None`
103
+ (default), which means the default precision for the backend, or a
104
+ `jax.lax.Precision` enum.
105
+
106
+ Returns:
107
+ The output array.
108
+ """
109
+
110
+ weight_reshaped = jax.lax.collapse(
111
+ weight, start_dimension=-2, stop_dimension=None
112
+ )
113
+ assert weight_reshaped.ndim == 2
114
+
115
+ y = jnp.dot(x, weight_reshaped, precision=precision)
116
+
117
+ # Apply activation and compute product of FP8/FP16/BF16 in FP32.
118
+ y = y.astype(jnp.promote_types(x.dtype, jnp.float32))
119
+ a, b = jnp.split(y, 2, axis=-1)
120
+ out = a * b if activation is None else activation(a) * b
121
+ out = out.astype(x.dtype)
122
+ return out
flax_model/alphafold3/jax/gated_linear_unit/matmul_ext.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Extended matmul ops."""
4
+
5
+ from collections.abc import Callable
6
+ import functools
7
+ from typing import Any, TypeAlias
8
+
9
+ from flax_model.alphafold3.jax.common import array_view
10
+ from flax_model.alphafold3.jax.common import triton_utils
11
+ from flax_model.alphafold3.jax.gated_linear_unit import block
12
+ from flax_model.alphafold3.jax.gated_linear_unit import matmul_config
13
+ import jax
14
+ from jax._src.state import discharge
15
+ from jax.experimental import pallas as pl
16
+ import jax.numpy as jnp
17
+ import jaxtyping
18
+ from jaxtyping import Array, Float, Int # pylint: disable=g-importing-member,g-multiple-import
19
+ import numpy as np
20
+ import typeguard
21
+
22
+ ArrayView = array_view.ArrayView
23
+ PyTree: TypeAlias = Any
24
+ ArrayT: TypeAlias = Any
25
+ ScalarInt: TypeAlias = (
26
+ Int[ArrayT, ''] | Int[np.generic, ''] | Int[jnp.generic, '']
27
+ )
28
+
29
+
30
+ def _get_group_cache_usage(
31
+ group_size_m, num_blocks_m, num_blocks_n, block_m_bytes, block_n_bytes
32
+ ) -> int:
33
+ """Returns the cache usage in bytes for the given group size."""
34
+ num_live_progs = jax.devices()[0].core_count
35
+ num_live_blocks_n = min(pl.cdiv(num_live_progs, group_size_m), num_blocks_n)
36
+ num_live_groups = pl.cdiv(num_live_progs, group_size_m * num_live_blocks_n)
37
+ num_live_blocks_m = min(num_live_groups * group_size_m, num_blocks_m)
38
+ return num_live_blocks_m * block_m_bytes + num_live_blocks_n * block_n_bytes
39
+
40
+
41
+ def _get_pids(
42
+ pid, num_blocks_m, num_blocks_n, group_size_m
43
+ ) -> tuple[ScalarInt, ScalarInt]:
44
+ """Returns the program IDs in each grid axis."""
45
+ # Use `floor_divide` and `remainder` (instead of lax.div and lax.rem)
46
+ # to handle dtypes: pid (int32) vs. num_blocks_n (int64) when `jax_enable_x64`
47
+ # is set.
48
+ if group_size_m == 1:
49
+ return jnp.floor_divide(pid, num_blocks_n), jnp.remainder(pid, num_blocks_n)
50
+
51
+ num_progs_in_group = group_size_m * num_blocks_n
52
+ group_start_m = jnp.floor_divide(pid, num_progs_in_group) * group_size_m
53
+ group_size_m = jnp.minimum(num_blocks_m - group_start_m, group_size_m)
54
+ pid_m = group_start_m + jnp.remainder(pid, group_size_m)
55
+ pid_n = jnp.floor_divide(jnp.remainder(pid, num_progs_in_group), group_size_m)
56
+ return pid_m, pid_n
57
+
58
+
59
+ def _get_best_pids(
60
+ pid, *, m, n, block_m, block_n, a_dtype_bytes, b_dtype_bytes
61
+ ) -> tuple[ScalarInt, ScalarInt]:
62
+ """Returns the grouped program IDs that minimize cache usage."""
63
+ num_blocks_m = pl.cdiv(m, block_m)
64
+ num_blocks_n = pl.cdiv(n, block_n)
65
+ block_m_bytes = block_m * a_dtype_bytes
66
+ block_n_bytes = block_n * b_dtype_bytes
67
+
68
+ num_live_progs = jax.devices()[0].core_count
69
+
70
+ def group_size_m_usage(group_size_m):
71
+ return _get_group_cache_usage(
72
+ group_size_m, num_blocks_m, num_blocks_n, block_m_bytes, block_n_bytes
73
+ )
74
+
75
+ group_size_m = min(
76
+ range(1, min(num_live_progs, num_blocks_m) + 1), key=group_size_m_usage
77
+ )
78
+
79
+ def group_size_n_usage(group_size_n):
80
+ return _get_group_cache_usage(
81
+ group_size_n, num_blocks_n, num_blocks_m, block_n_bytes, block_m_bytes
82
+ )
83
+
84
+ group_size_n = min(
85
+ range(1, min(num_live_progs, num_blocks_n) + 1), key=group_size_n_usage
86
+ )
87
+
88
+ if group_size_m_usage(group_size_m) <= group_size_n_usage(group_size_n):
89
+ pid_m, pid_n = _get_pids(pid, num_blocks_m, num_blocks_n, group_size_m)
90
+ else:
91
+ pid_n, pid_m = _get_pids(pid, num_blocks_n, num_blocks_m, group_size_n)
92
+ return pid_m, pid_n
93
+
94
+
95
+ def _apply_epilogue(
96
+ epilogue: Callable[..., jax.Array], x: jax.Array, args: PyTree
97
+ ) -> jax.Array:
98
+ """Applies the epilogue to the output."""
99
+ # Convert array view arguments to JAX arrays. This means that we can use the
100
+ # array view slices, rather than the gather that discharging state gives us.
101
+ is_leaf = lambda x: isinstance(x, ArrayView)
102
+ args_flat, args_tree = jax.tree.flatten((x, args), is_leaf=is_leaf)
103
+ args_flat = tuple(map(jnp.array, args_flat))
104
+
105
+ def epilogue_wrapper(refs):
106
+ x_ref, arg_refs = args_tree.unflatten(refs)
107
+ x_ref[:] = epilogue(x_ref[:], arg_refs, 0, 0)
108
+
109
+ return discharge.run_state_reference(epilogue_wrapper)(args_flat)[0]
110
+
111
+
112
+ def _gated_linear_unit_kernel(
113
+ x_ref,
114
+ w_ref,
115
+ v_ref,
116
+ _, # Destination, aliased with `out_ref`.
117
+ epilogue_in_refs,
118
+ out_ref,
119
+ *,
120
+ block_m,
121
+ block_n,
122
+ block_k,
123
+ activation,
124
+ precision,
125
+ epilogue,
126
+ ):
127
+ """Pallas GLU kernel."""
128
+ m = x_ref.shape[0]
129
+ n = w_ref.shape[1]
130
+ pid_m, pid_n = _get_best_pids(
131
+ pl.program_id(0),
132
+ m=m,
133
+ n=n,
134
+ block_m=block_m,
135
+ block_n=block_n,
136
+ a_dtype_bytes=jnp.dtype(x_ref.dtype).itemsize,
137
+ b_dtype_bytes=jnp.dtype(w_ref.dtype).itemsize * 2, # Two blocks.
138
+ )
139
+
140
+ def body(i, acc):
141
+ x = block.load_block(x_ref, (pid_m, i), block_shape=(block_m, block_k))
142
+ w = block.load_block(w_ref, (i, pid_n), block_shape=(block_k, block_n))
143
+ v = block.load_block(v_ref, (i, pid_n), block_shape=(block_k, block_n))
144
+ acc[0] += pl.dot(x, w.astype(x.dtype), precision=precision)
145
+ acc[1] += pl.dot(x, v.astype(x.dtype), precision=precision)
146
+ return acc
147
+
148
+ num_iters = pl.cdiv(x_ref.shape[-1], block_k)
149
+ acc0 = jnp.zeros((block_m, block_n), dtype=jnp.float32)
150
+ acc1 = jnp.zeros((block_m, block_n), dtype=jnp.float32)
151
+ proj, gates = jax.lax.fori_loop(0, num_iters, body, init_val=[acc0, acc1])
152
+
153
+ proj = proj.astype(x_ref.dtype).astype(jnp.float32)
154
+ gates = gates.astype(x_ref.dtype).astype(jnp.float32)
155
+
156
+ out = proj * (gates if activation is None else activation(gates))
157
+
158
+ if epilogue is not None:
159
+ out = epilogue(out, epilogue_in_refs, pid_m, pid_n)
160
+
161
+ block.store_block(out_ref, out, (pid_m, pid_n))
162
+
163
+
164
+ def _gated_linear_unit(
165
+ x: Float[Array | ArrayView, 'M K'],
166
+ weights_projection: Float[Array | ArrayView, 'K N'],
167
+ weights_gate: Float[Array | ArrayView, 'K N'],
168
+ *,
169
+ dst: Float[ArrayView, 'M N'] | None = None,
170
+ activation: Callable[[jax.Array], jax.Array] | None,
171
+ epilogue: Any, # Callable[..., Any] | None - breaks `typed`.
172
+ epilogue_args: PyTree,
173
+ precision: jax.lax.Precision | None,
174
+ ) -> jax.Array: # Float[Array, 'M N'] | Float[Array, 'N M']
175
+ """Applies a gated linear unit (arxiv.org/abs/1612.08083)."""
176
+ if epilogue is None and epilogue_args is not None:
177
+ raise ValueError('`epilogue_args` is specified but `epilogue` is None.')
178
+
179
+ name = 'pallas_glu'
180
+ if activation is not None:
181
+ name += f'_{getattr(activation, "__name__", repr(activation))}'
182
+ if epilogue is not None:
183
+ name += f'_{getattr(epilogue, "__name__", repr(epilogue))}'
184
+
185
+ w = weights_projection
186
+ config = matmul_config.get_config(x, w)
187
+
188
+ m = x.shape[0]
189
+ n = w.shape[1]
190
+ kernel = functools.partial(
191
+ _gated_linear_unit_kernel,
192
+ block_m=config.block_m,
193
+ block_n=config.block_n,
194
+ block_k=config.block_k,
195
+ activation=activation,
196
+ precision=precision,
197
+ epilogue=epilogue,
198
+ )
199
+
200
+ if dst is None:
201
+ input_output_aliases = {}
202
+ else:
203
+ input_output_aliases = {3: 0}
204
+
205
+ compiler_params = dict(
206
+ triton=dict(num_warps=config.num_warps, num_stages=config.num_stages)
207
+ )
208
+
209
+ return pl.pallas_call(
210
+ kernel,
211
+ name=name,
212
+ grid=(pl.cdiv(m, config.block_m) * pl.cdiv(n, config.block_n),),
213
+ out_shape=jax.ShapeDtypeStruct((m, n), x.dtype) if dst is None else dst,
214
+ input_output_aliases=input_output_aliases,
215
+ compiler_params=compiler_params,
216
+ backend='triton',
217
+ )(x, weights_projection, weights_gate, dst, epilogue_args)
218
+
219
+
220
+ @jaxtyping.jaxtyped(typechecker=typeguard.typechecked)
221
+ def gated_linear_unit(
222
+ x: Float[Array | ArrayView, '*B M K'],
223
+ weights_projection: Float[Array | ArrayView, 'K N'],
224
+ weights_gate: Float[Array | ArrayView, 'K N'],
225
+ *,
226
+ activation: Callable[[jax.Array], jax.Array] | None = None,
227
+ precision: jax.lax.Precision | None = None,
228
+ ) -> Float[Array | ArrayView, '*B M N']:
229
+ """Applies a gated linear unit (arxiv.org/abs/1612.08083).
230
+
231
+ Args:
232
+ x: Input activations.
233
+ weights_projection: Weights for linear projection.
234
+ weights_gate: Weights for gates.
235
+ activation: Optional activation function.
236
+ precision: Specifies the precision of the matmuls.
237
+
238
+ Returns:
239
+ `(x @ weights_projection) * activation(x @ weights_gate)`
240
+ """
241
+
242
+ supported_dtypes = {'float16', 'bfloat16', 'float32'}
243
+ if x.dtype.name not in supported_dtypes:
244
+ raise NotImplementedError(
245
+ f'Triton kernel does not support input datatype {x.dtype.name}. Must be'
246
+ f' one of {supported_dtypes}.'
247
+ )
248
+
249
+ if not triton_utils.has_triton_support():
250
+ raise NotImplementedError('Triton kernel not supported on current device.')
251
+
252
+ *batch, m, _ = x.shape
253
+ n = weights_projection.shape[1]
254
+ x = array_view.as_array_view(x).collapse(start=0, stop=-1)
255
+
256
+ return _gated_linear_unit(
257
+ x,
258
+ weights_projection,
259
+ weights_gate,
260
+ dst=None,
261
+ activation=activation,
262
+ precision=precision,
263
+ epilogue=None,
264
+ epilogue_args=None,
265
+ ).reshape(batch + [m, n])
flax_model/alphafold3/jax/geometry/rigid_matrix_vector.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Rigid3Array Transformations represented by a Matrix and a Vector."""
4
+
5
+ from typing import Any, Final, Self, TypeAlias
6
+
7
+ from flax_model.alphafold3.jax.geometry import rotation_matrix
8
+ from flax_model.alphafold3.jax.geometry import struct_of_array
9
+ from flax_model.alphafold3.jax.geometry import utils
10
+ from flax_model.alphafold3.jax.geometry import vector
11
+ import jax
12
+ import jax.numpy as jnp
13
+
14
+
15
+ Float: TypeAlias = float | jnp.ndarray
16
+
17
+ VERSION: Final[str] = '0.1'
18
+
19
+
20
+ # Disabling name in pylint, since the relevant variable in math are typically
21
+ # referred to as X, Y in mathematical literature.
22
+ def _compute_covariance_matrix(
23
+ row_values: vector.Vec3Array,
24
+ col_values: vector.Vec3Array,
25
+ weights: jnp.ndarray,
26
+ epsilon=1e-6,
27
+ ) -> jnp.ndarray:
28
+ """Compute covariance matrix.
29
+
30
+ The quantity computes is
31
+ cov_xy = weighted_avg_i(row_values[i, x] col_values[j, y]).
32
+ Here x and y run over the xyz coordinates.
33
+ This is used to construct frames when aligning points.
34
+
35
+ Args:
36
+ row_values: Values used for rows of covariance matrix, shape [..., n_point]
37
+ col_values: Values used for columns of covariance matrix, shape [...,
38
+ n_point]
39
+ weights: weights to weight points by, shape broacastable to [...]
40
+ epsilon: small value to add to denominator to avoid Nan's when all weights
41
+ are 0.
42
+
43
+ Returns:
44
+ Covariance Matrix as [..., 3, 3] array.
45
+ """
46
+ weights = jnp.asarray(weights)
47
+ weights = jnp.broadcast_to(weights, row_values.shape)
48
+
49
+ out = []
50
+
51
+ normalized_weights = weights / (weights.sum(axis=-1, keepdims=True) + epsilon)
52
+
53
+ weighted_average = lambda x: jnp.sum(normalized_weights * x, axis=-1)
54
+
55
+ out.append(
56
+ jnp.stack(
57
+ (
58
+ weighted_average(row_values.x * col_values.x),
59
+ weighted_average(row_values.x * col_values.y),
60
+ weighted_average(row_values.x * col_values.z),
61
+ ),
62
+ axis=-1,
63
+ )
64
+ )
65
+
66
+ out.append(
67
+ jnp.stack(
68
+ (
69
+ weighted_average(row_values.y * col_values.x),
70
+ weighted_average(row_values.y * col_values.y),
71
+ weighted_average(row_values.y * col_values.z),
72
+ ),
73
+ axis=-1,
74
+ )
75
+ )
76
+
77
+ out.append(
78
+ jnp.stack(
79
+ (
80
+ weighted_average(row_values.z * col_values.x),
81
+ weighted_average(row_values.z * col_values.y),
82
+ weighted_average(row_values.z * col_values.z),
83
+ ),
84
+ axis=-1,
85
+ )
86
+ )
87
+
88
+ return jnp.stack(out, axis=-2)
89
+
90
+
91
+ @struct_of_array.StructOfArray(same_dtype=True)
92
+ class Rigid3Array:
93
+ """Rigid Transformation, i.e. element of special euclidean group."""
94
+
95
+ rotation: rotation_matrix.Rot3Array
96
+ translation: vector.Vec3Array
97
+
98
+ def __matmul__(self, other: Self) -> Self:
99
+ new_rotation = self.rotation @ other.rotation
100
+ new_translation = self.apply_to_point(other.translation)
101
+ return Rigid3Array(new_rotation, new_translation)
102
+
103
+ def inverse(self) -> Self:
104
+ """Return Rigid3Array corresponding to inverse transform."""
105
+ inv_rotation = self.rotation.inverse()
106
+ inv_translation = inv_rotation.apply_to_point(-self.translation)
107
+ return Rigid3Array(inv_rotation, inv_translation)
108
+
109
+ def apply_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array:
110
+ """Apply Rigid3Array transform to point."""
111
+ return self.rotation.apply_to_point(point) + self.translation
112
+
113
+ def apply_inverse_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array:
114
+ """Apply inverse Rigid3Array transform to point."""
115
+ new_point = point - self.translation
116
+ return self.rotation.apply_inverse_to_point(new_point)
117
+
118
+ def compose_rotation(self, other_rotation: rotation_matrix.Rot3Array) -> Self:
119
+ rot = self.rotation @ other_rotation
120
+ trans = jax.tree.map(
121
+ lambda x: jnp.broadcast_to(x, rot.shape), self.translation
122
+ )
123
+ return Rigid3Array(rot, trans)
124
+
125
+ @classmethod
126
+ def identity(cls, shape: Any, dtype: jnp.dtype = jnp.float32) -> Self:
127
+ """Return identity Rigid3Array of given shape."""
128
+ return cls(
129
+ rotation_matrix.Rot3Array.identity(shape, dtype=dtype),
130
+ vector.Vec3Array.zeros(shape, dtype=dtype),
131
+ ) # pytype: disable=wrong-arg-count # trace-all-classes
132
+
133
+ def scale_translation(self, factor: Float) -> Self:
134
+ """Scale translation in Rigid3Array by 'factor'."""
135
+ return Rigid3Array(self.rotation, self.translation * factor)
136
+
137
+ def to_array(self):
138
+ rot_array = self.rotation.to_array()
139
+ vec_array = self.translation.to_array()
140
+ return jnp.concatenate([rot_array, vec_array[..., None]], axis=-1)
141
+
142
+ @classmethod
143
+ def from_array(cls, array):
144
+ rot = rotation_matrix.Rot3Array.from_array(array[..., :3])
145
+ vec = vector.Vec3Array.from_array(array[..., -1])
146
+ return cls(rot, vec) # pytype: disable=wrong-arg-count # trace-all-classes
147
+
148
+ @classmethod
149
+ def from_array4x4(cls, array: jnp.ndarray) -> Self:
150
+ """Construct Rigid3Array from homogeneous 4x4 array."""
151
+ if array.shape[-2:] != (4, 4):
152
+ raise ValueError(f'array.shape({array.shape}) must be [..., 4, 4]')
153
+ rotation = rotation_matrix.Rot3Array(
154
+ *(array[..., 0, 0], array[..., 0, 1], array[..., 0, 2]),
155
+ *(array[..., 1, 0], array[..., 1, 1], array[..., 1, 2]),
156
+ *(array[..., 2, 0], array[..., 2, 1], array[..., 2, 2]),
157
+ )
158
+ translation = vector.Vec3Array(
159
+ array[..., 0, 3], array[..., 1, 3], array[..., 2, 3]
160
+ )
161
+ return cls(rotation, translation) # pytype: disable=wrong-arg-count # trace-all-classes
162
+
163
+ @classmethod
164
+ def from_point_alignment(
165
+ cls,
166
+ points_to: vector.Vec3Array,
167
+ points_from: vector.Vec3Array,
168
+ weights: Float | None = None,
169
+ epsilon: float = 1e-6,
170
+ ) -> Self:
171
+ """Constructs Rigid3Array by finding transform aligning points.
172
+
173
+ This constructs the optimal Rigid Transform taking points_from to the
174
+ arrangement closest to points_to.
175
+
176
+ Args:
177
+ points_to: Points to align to.
178
+ points_from: Points to align from.
179
+ weights: weights for points.
180
+ epsilon: epsilon used to regularize covariance matrix.
181
+
182
+ Returns:
183
+ Rigid Transform.
184
+ """
185
+ if weights is None:
186
+ weights = 1.0
187
+
188
+ def compute_center(value):
189
+ return utils.weighted_mean(value=value, weights=weights, axis=-1)
190
+
191
+ points_to_center = jax.tree.map(compute_center, points_to)
192
+ points_from_center = jax.tree.map(compute_center, points_from)
193
+ centered_points_to = points_to - points_to_center[..., None]
194
+ centered_points_from = points_from - points_from_center[..., None]
195
+ cov_mat = _compute_covariance_matrix(
196
+ centered_points_to,
197
+ centered_points_from,
198
+ weights=weights,
199
+ epsilon=epsilon,
200
+ )
201
+ rots = rotation_matrix.Rot3Array.from_svd(
202
+ jnp.reshape(cov_mat, cov_mat.shape[:-2] + (9,))
203
+ )
204
+
205
+ translations = points_to_center - rots.apply_to_point(points_from_center)
206
+
207
+ return cls(rots, translations) # pytype: disable=wrong-arg-count # trace-all-classes
208
+
209
+ def __getstate__(self):
210
+ return (VERSION, (self.rotation, self.translation))
211
+
212
+ def __setstate__(self, state):
213
+ version, (rot, trans) = state
214
+ del version
215
+ object.__setattr__(self, 'rotation', rot)
216
+ object.__setattr__(self, 'translation', trans)
flax_model/alphafold3/jax/geometry/rotation_matrix.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Rot3Array Matrix Class."""
4
+
5
+ import dataclasses
6
+ from typing import Any, Final, Self
7
+
8
+ from flax_model.alphafold3.jax.geometry import struct_of_array
9
+ from flax_model.alphafold3.jax.geometry import utils
10
+ from flax_model.alphafold3.jax.geometry import vector
11
+ import jax
12
+ import jax.numpy as jnp
13
+ import numpy as np
14
+
15
+
16
+ COMPONENTS: Final[tuple[str, ...]] = (
17
+ *('xx', 'xy', 'xz'),
18
+ *('yx', 'yy', 'yz'),
19
+ *('zx', 'zy', 'zz'),
20
+ )
21
+ VERSION: Final[str] = '0.1'
22
+
23
+
24
+ def make_matrix_svd_factors() -> np.ndarray:
25
+ """Generates factors for converting 3x3 matrix to symmetric 4x4 matrix."""
26
+ factors = np.zeros((16, 9), dtype=np.float32)
27
+
28
+ factors[0, [0, 4, 8]] = 1.0
29
+
30
+ factors[[1, 4], 5] = 1.0
31
+ factors[[1, 4], 7] = -1.0
32
+
33
+ factors[[2, 8], 6] = 1.0
34
+ factors[[2, 8], 2] = -1.0
35
+
36
+ factors[[3, 12], 1] = 1.0
37
+ factors[[3, 12], 3] = -1.0
38
+
39
+ factors[5, 0] = 1.0
40
+ factors[5, [4, 8]] = -1.0
41
+
42
+ factors[[6, 9], 1] = 1.0
43
+ factors[[6, 9], 3] = 1.0
44
+
45
+ factors[[7, 13], 2] = 1.0
46
+ factors[[7, 13], 6] = 1.0
47
+
48
+ factors[10, 4] = 1.0
49
+ factors[10, [0, 8]] = -1.0
50
+
51
+ factors[[11, 14], 5] = 1.0
52
+ factors[[11, 14], 7] = 1.0
53
+
54
+ factors[15, 8] = 1.0
55
+ factors[15, [0, 4]] = -1.0
56
+
57
+ return factors
58
+
59
+
60
+ @jax.custom_jvp
61
+ def largest_evec(m):
62
+ _, eigvecs = jnp.linalg.eigh(m)
63
+ return eigvecs[..., -1]
64
+
65
+
66
+ def largest_evec_jvp(primals, tangents):
67
+ """jvp for largest eigenvector."""
68
+ (m,) = primals
69
+ (t,) = tangents
70
+ eigvals, eigvecs = jnp.linalg.eigh(m)
71
+ large_eigvec = eigvecs[..., -1]
72
+ large_eigval = eigvals[..., -1]
73
+ other_eigvals = eigvals[..., :-1]
74
+ other_eigvecs = eigvecs[..., :-1]
75
+ other_ev_times_tangent = jnp.einsum(
76
+ '...aj,...ab -> ...bj',
77
+ other_eigvecs,
78
+ t,
79
+ precision=jax.lax.Precision.HIGHEST,
80
+ )
81
+ nominator = jnp.einsum(
82
+ '...bj,...b -> ...j',
83
+ other_ev_times_tangent,
84
+ large_eigvec,
85
+ precision=jax.lax.Precision.HIGHEST,
86
+ )
87
+ prefactor = nominator / jnp.maximum(
88
+ large_eigval[..., None] - other_eigvals, 1e-6
89
+ )
90
+ grad = jnp.sum(prefactor[..., None, :] * other_eigvecs, axis=-1)
91
+ return large_eigvec, grad
92
+
93
+
94
+ largest_evec.defjvp(largest_evec_jvp)
95
+
96
+
97
+ MATRIX_SVD_QUAT_FACTORS = make_matrix_svd_factors()
98
+
99
+
100
+ @struct_of_array.StructOfArray(same_dtype=True)
101
+ class Rot3Array:
102
+ """Rot3Array Matrix in 3 dimensional Space implemented as struct of arrays."""
103
+
104
+ xx: jnp.ndarray = dataclasses.field(metadata={'dtype': jnp.float32})
105
+ xy: jnp.ndarray
106
+ xz: jnp.ndarray
107
+ yx: jnp.ndarray
108
+ yy: jnp.ndarray
109
+ yz: jnp.ndarray
110
+ zx: jnp.ndarray
111
+ zy: jnp.ndarray
112
+ zz: jnp.ndarray
113
+
114
+ __array_ufunc__ = None
115
+
116
+ def inverse(self) -> Self:
117
+ """Returns inverse of Rot3Array."""
118
+ return Rot3Array(
119
+ *(self.xx, self.yx, self.zx),
120
+ *(self.xy, self.yy, self.zy),
121
+ *(self.xz, self.yz, self.zz),
122
+ )
123
+
124
+ def apply_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array:
125
+ """Applies Rot3Array to point."""
126
+ return vector.Vec3Array(
127
+ self.xx * point.x + self.xy * point.y + self.xz * point.z,
128
+ self.yx * point.x + self.yy * point.y + self.yz * point.z,
129
+ self.zx * point.x + self.zy * point.y + self.zz * point.z,
130
+ )
131
+
132
+ def apply_inverse_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array:
133
+ """Applies inverse Rot3Array to point."""
134
+ return self.inverse().apply_to_point(point)
135
+
136
+ def __matmul__(self, other: Self) -> Self:
137
+ """Composes two Rot3Arrays."""
138
+ c0 = self.apply_to_point(vector.Vec3Array(other.xx, other.yx, other.zx))
139
+ c1 = self.apply_to_point(vector.Vec3Array(other.xy, other.yy, other.zy))
140
+ c2 = self.apply_to_point(vector.Vec3Array(other.xz, other.yz, other.zz))
141
+ return Rot3Array(c0.x, c1.x, c2.x, c0.y, c1.y, c2.y, c0.z, c1.z, c2.z)
142
+
143
+ @classmethod
144
+ def identity(cls, shape: Any, dtype: jnp.dtype = jnp.float32) -> Self:
145
+ """Returns identity of given shape."""
146
+ ones = jnp.ones(shape, dtype=dtype)
147
+ zeros = jnp.zeros(shape, dtype=dtype)
148
+ return cls(ones, zeros, zeros, zeros, ones, zeros, zeros, zeros, ones) # pytype: disable=wrong-arg-count # trace-all-classes
149
+
150
+ @classmethod
151
+ def from_two_vectors(cls, e0: vector.Vec3Array, e1: vector.Vec3Array) -> Self:
152
+ """Construct Rot3Array from two Vectors.
153
+
154
+ Rot3Array is constructed such that in the corresponding frame 'e0' lies on
155
+ the positive x-Axis and 'e1' lies in the xy plane with positive sign of y.
156
+
157
+ Args:
158
+ e0: Vector
159
+ e1: Vector
160
+
161
+ Returns:
162
+ Rot3Array
163
+ """
164
+ # Normalize the unit vector for the x-axis, e0.
165
+ e0 = e0.normalized()
166
+ # make e1 perpendicular to e0.
167
+ c = e1.dot(e0)
168
+ e1 = (e1 - c * e0).normalized()
169
+ # Compute e2 as cross product of e0 and e1.
170
+ e2 = e0.cross(e1)
171
+ return cls(e0.x, e1.x, e2.x, e0.y, e1.y, e2.y, e0.z, e1.z, e2.z) # pytype: disable=wrong-arg-count # trace-all-classes
172
+
173
+ @classmethod
174
+ def from_array(cls, array: jnp.ndarray) -> Self:
175
+ """Construct Rot3Array Matrix from array of shape. [..., 3, 3]."""
176
+ unstacked = utils.unstack(array, axis=-2)
177
+ unstacked = sum([utils.unstack(x, axis=-1) for x in unstacked], [])
178
+ return cls(*unstacked)
179
+
180
+ def to_array(self) -> jnp.ndarray:
181
+ """Convert Rot3Array to array of shape [..., 3, 3]."""
182
+ return jnp.stack(
183
+ [
184
+ jnp.stack([self.xx, self.xy, self.xz], axis=-1),
185
+ jnp.stack([self.yx, self.yy, self.yz], axis=-1),
186
+ jnp.stack([self.zx, self.zy, self.zz], axis=-1),
187
+ ],
188
+ axis=-2,
189
+ )
190
+
191
+ @classmethod
192
+ def from_quaternion(
193
+ cls,
194
+ w: jnp.ndarray,
195
+ x: jnp.ndarray,
196
+ y: jnp.ndarray,
197
+ z: jnp.ndarray,
198
+ normalize: bool = True,
199
+ epsilon: float = 1e-6,
200
+ ) -> Self:
201
+ """Construct Rot3Array from components of quaternion."""
202
+ if normalize:
203
+ inv_norm = jax.lax.rsqrt(jnp.maximum(epsilon, w**2 + x**2 + y**2 + z**2))
204
+ w *= inv_norm
205
+ x *= inv_norm
206
+ y *= inv_norm
207
+ z *= inv_norm
208
+ xx = 1 - 2 * (jnp.square(y) + jnp.square(z))
209
+ xy = 2 * (x * y - w * z)
210
+ xz = 2 * (x * z + w * y)
211
+ yx = 2 * (x * y + w * z)
212
+ yy = 1 - 2 * (jnp.square(x) + jnp.square(z))
213
+ yz = 2 * (y * z - w * x)
214
+ zx = 2 * (x * z - w * y)
215
+ zy = 2 * (y * z + w * x)
216
+ zz = 1 - 2 * (jnp.square(x) + jnp.square(y))
217
+ return cls(xx, xy, xz, yx, yy, yz, zx, zy, zz) # pytype: disable=wrong-arg-count # trace-all-classes
218
+
219
+ @classmethod
220
+ def from_svd(cls, mat: jnp.ndarray, use_quat_formula: bool = True) -> Self:
221
+ """Constructs Rot3Array from arbitrary array of shape [3 * 3] using SVD.
222
+
223
+ The case when 'use_quat_formula' is False rephrases the problem of
224
+ projecting the matrix to a rotation matrix as a problem of finding the
225
+ largest eigenvector of a certain 4x4 matrix. This has the advantage of
226
+ having fewer numerical issues.
227
+ This approach follows:
228
+ https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.65.971&rep=rep1&type=pdf
229
+ In the other case we construct it via svd following
230
+ https://arxiv.org/pdf/2006.14616.pdf
231
+ In that case [∂L/∂M] is large if the two smallest singular values are close
232
+ to each other, or if they are close to 0.
233
+
234
+ Args:
235
+ mat: Array of shape [..., 3 * 3]
236
+ use_quat_formula: Whether to construct matrix via 4x4 eigenvalue problem.
237
+
238
+ Returns:
239
+ Rot3Array of shape [...]
240
+ """
241
+ assert mat.shape[-1] == 9
242
+ if use_quat_formula:
243
+ symmetric_4by4 = jnp.einsum(
244
+ 'ji, ...i -> ...j',
245
+ MATRIX_SVD_QUAT_FACTORS,
246
+ mat,
247
+ precision=jax.lax.Precision.HIGHEST,
248
+ )
249
+ symmetric_4by4 = jnp.reshape(symmetric_4by4, mat.shape[:-1] + (4, 4))
250
+ largest_eigvec = largest_evec(symmetric_4by4)
251
+ return cls.from_quaternion(
252
+ *utils.unstack(largest_eigvec, axis=-1)
253
+ ).inverse()
254
+
255
+ else:
256
+ mat = jnp.reshape(mat, mat.shape[:-1] + (3, 3))
257
+ u, _, v_t = jnp.linalg.svd(mat, full_matrices=False)
258
+ det_uv_t = jnp.linalg.det(
259
+ jnp.matmul(u, v_t, precision=jax.lax.Precision.HIGHEST)
260
+ )
261
+ ones = jnp.ones_like(det_uv_t)
262
+ diag_array = jnp.stack([ones, ones, det_uv_t], axis=-1)
263
+ # This is equivalent to making diag_array into a diagonal array and matrix
264
+ # multiplying
265
+ diag_times_v_t = diag_array[..., None] * v_t
266
+ out = jnp.matmul(u, diag_times_v_t, precision=jax.lax.Precision.HIGHEST)
267
+ return cls.from_array(out)
268
+
269
+ @classmethod
270
+ def random_uniform(cls, key, shape, dtype=jnp.float32) -> Self:
271
+ """Samples uniform random Rot3Array according to Haar Measure."""
272
+ quat_array = jax.random.normal(key, tuple(shape) + (4,), dtype=dtype)
273
+ quats = utils.unstack(quat_array)
274
+ return cls.from_quaternion(*quats)
275
+
276
+ def __getstate__(self):
277
+ return (VERSION, [np.asarray(getattr(self, field)) for field in COMPONENTS])
278
+
279
+ def __setstate__(self, state):
280
+ version, state = state
281
+ del version
282
+ for i, field in enumerate(COMPONENTS):
283
+ object.__setattr__(self, field, state[i])
flax_model/alphafold3/jax/geometry/struct_of_array.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Class decorator to represent (nested) struct of arrays."""
4
+
5
+ import dataclasses
6
+
7
+ import jax
8
+
9
+
10
+ def get_item(instance, key):
11
+ sliced = {}
12
+ for field in get_array_fields(instance):
13
+ num_trailing_dims = field.metadata.get('num_trailing_dims', 0)
14
+ this_key = key
15
+ if isinstance(key, tuple) and Ellipsis in this_key:
16
+ this_key += (slice(None),) * num_trailing_dims
17
+ sliced[field.name] = jax.tree.map(
18
+ lambda x: x[this_key], # pylint: disable=cell-var-from-loop
19
+ getattr(instance, field.name),
20
+ )
21
+ return dataclasses.replace(instance, **sliced)
22
+
23
+
24
+ @property
25
+ def get_shape(instance):
26
+ """Returns Shape for given instance of dataclass."""
27
+ first_field = dataclasses.fields(instance)[0]
28
+ num_trailing_dims = first_field.metadata.get('num_trailing_dims', None)
29
+ value = getattr(instance, first_field.name)
30
+ if num_trailing_dims:
31
+ return value.shape[:-num_trailing_dims]
32
+ else:
33
+ return value.shape
34
+
35
+
36
+ def get_len(instance):
37
+ """Returns length for given instance of dataclass."""
38
+ shape = instance.shape
39
+ if shape:
40
+ return shape[0]
41
+ else:
42
+ raise TypeError('len() of unsized object') # Match jax.numpy behavior.
43
+
44
+
45
+ @property
46
+ def get_dtype(instance):
47
+ """Returns Dtype for given instance of dataclass."""
48
+ fields = dataclasses.fields(instance)
49
+ sets_dtype = [
50
+ field.name for field in fields if field.metadata.get('sets_dtype', False)
51
+ ]
52
+ if sets_dtype:
53
+ assert len(sets_dtype) == 1, 'at most field can set dtype'
54
+ field_value = getattr(instance, sets_dtype[0])
55
+ elif instance.same_dtype:
56
+ field_value = getattr(instance, fields[0].name)
57
+ else:
58
+ # Should this be Value Error?
59
+ raise AttributeError(
60
+ 'Trying to access Dtype on Struct of Array without'
61
+ 'either "same_dtype" or field setting dtype'
62
+ )
63
+
64
+ if hasattr(field_value, 'dtype'):
65
+ return field_value.dtype
66
+ else:
67
+ # Should this be Value Error?
68
+ raise AttributeError(f'field_value {field_value} does not have dtype')
69
+
70
+
71
+ def replace(instance, **kwargs):
72
+ return dataclasses.replace(instance, **kwargs)
73
+
74
+
75
+ def post_init(instance):
76
+ """Validate instance has same shapes & dtypes."""
77
+ array_fields = get_array_fields(instance)
78
+ arrays = list(get_array_fields(instance, return_values=True).values())
79
+ first_field = array_fields[0]
80
+ # These slightly weird constructions about checking whether the leaves are
81
+ # actual arrays is since e.g. vmap internally relies on being able to
82
+ # construct pytree's with object() as leaves, this would break the checking
83
+ # as such we are only validating the object when the entries in the dataclass
84
+ # Are arrays or other dataclasses of arrays.
85
+ try:
86
+ dtype = instance.dtype
87
+ except AttributeError:
88
+ dtype = None
89
+ if dtype is not None:
90
+ first_shape = instance.shape
91
+ for array, field in zip(arrays, array_fields, strict=True):
92
+ num_trailing_dims = field.metadata.get('num_trailing_dims', None)
93
+ if num_trailing_dims:
94
+ array_shape = array.shape
95
+ field_shape = array_shape[:-num_trailing_dims]
96
+ msg = (
97
+ f'field {field} should have number of trailing dims'
98
+ ' {num_trailing_dims}'
99
+ )
100
+ assert len(array_shape) == len(first_shape) + num_trailing_dims, msg
101
+ else:
102
+ field_shape = array.shape
103
+
104
+ shape_msg = (
105
+ f"Stripped Shape {field_shape} of field {field} doesn't "
106
+ f'match shape {first_shape} of field {first_field}'
107
+ )
108
+ assert field_shape == first_shape, shape_msg
109
+
110
+ field_dtype = array.dtype
111
+
112
+ allowed_metadata_dtypes = field.metadata.get('allowed_dtypes', [])
113
+ if allowed_metadata_dtypes:
114
+ msg = f'Dtype is {field_dtype} but must be in {allowed_metadata_dtypes}'
115
+ assert field_dtype in allowed_metadata_dtypes, msg
116
+
117
+ if 'dtype' in field.metadata:
118
+ target_dtype = field.metadata['dtype']
119
+ else:
120
+ target_dtype = dtype
121
+
122
+ msg = f'Dtype is {field_dtype} but must be {target_dtype}'
123
+ assert field_dtype == target_dtype, msg
124
+
125
+
126
+ def flatten(instance):
127
+ """Flatten Struct of Array instance."""
128
+ array_likes = get_array_fields(instance, return_values=True).values()
129
+ flat_array_likes = []
130
+ inner_treedefs = []
131
+ num_arrays = []
132
+ for array_like in array_likes:
133
+ flat_array_like, inner_treedef = jax.tree_util.tree_flatten(array_like)
134
+ inner_treedefs.append(inner_treedef)
135
+ flat_array_likes += flat_array_like
136
+ num_arrays.append(len(flat_array_like))
137
+ metadata = get_metadata_fields(instance, return_values=True)
138
+ metadata = type(instance).metadata_cls(**metadata)
139
+ return flat_array_likes, (inner_treedefs, metadata, num_arrays)
140
+
141
+
142
+ def make_metadata_class(cls):
143
+ metadata_fields = get_fields(
144
+ cls, lambda x: x.metadata.get('is_metadata', False)
145
+ )
146
+ metadata_cls = dataclasses.make_dataclass(
147
+ cls_name='Meta' + cls.__name__,
148
+ fields=[(field.name, field.type, field) for field in metadata_fields],
149
+ frozen=True,
150
+ eq=True,
151
+ )
152
+ return metadata_cls
153
+
154
+
155
+ def get_fields(cls_or_instance, filterfn, return_values=False):
156
+ fields = dataclasses.fields(cls_or_instance)
157
+ fields = [field for field in fields if filterfn(field)]
158
+ if return_values:
159
+ return {
160
+ field.name: getattr(cls_or_instance, field.name) for field in fields
161
+ }
162
+ else:
163
+ return fields
164
+
165
+
166
+ def get_array_fields(cls, return_values=False):
167
+ return get_fields(
168
+ cls,
169
+ lambda x: not x.metadata.get('is_metadata', False),
170
+ return_values=return_values,
171
+ )
172
+
173
+
174
+ def get_metadata_fields(cls, return_values=False):
175
+ return get_fields(
176
+ cls,
177
+ lambda x: x.metadata.get('is_metadata', False),
178
+ return_values=return_values,
179
+ )
180
+
181
+
182
+ class StructOfArray:
183
+ """Class Decorator for Struct Of Arrays."""
184
+
185
+ def __init__(self, same_dtype=True):
186
+ self.same_dtype = same_dtype
187
+
188
+ def __call__(self, cls):
189
+ cls.__array_ufunc__ = None
190
+ cls.replace = replace
191
+ cls.same_dtype = self.same_dtype
192
+ cls.dtype = get_dtype
193
+ cls.shape = get_shape
194
+ cls.__len__ = get_len
195
+ cls.__getitem__ = get_item
196
+ cls.__post_init__ = post_init
197
+ new_cls = dataclasses.dataclass(cls, frozen=True, eq=False) # pytype: disable=wrong-keyword-args
198
+ # pytree claims to require metadata to be hashable, not sure why,
199
+ # But making derived dataclass that can just hold metadata
200
+ new_cls.metadata_cls = make_metadata_class(new_cls)
201
+
202
+ def unflatten(aux, data):
203
+ inner_treedefs, metadata, num_arrays = aux
204
+ array_fields = [field.name for field in get_array_fields(new_cls)]
205
+ value_dict = {}
206
+ array_start = 0
207
+ for num_array, inner_treedef, array_field in zip(
208
+ num_arrays, inner_treedefs, array_fields, strict=True
209
+ ):
210
+ value_dict[array_field] = jax.tree_util.tree_unflatten(
211
+ inner_treedef, data[array_start : array_start + num_array]
212
+ )
213
+ array_start += num_array
214
+ metadata_fields = get_metadata_fields(new_cls)
215
+ for field in metadata_fields:
216
+ value_dict[field.name] = getattr(metadata, field.name)
217
+
218
+ return new_cls(**value_dict)
219
+
220
+ jax.tree_util.register_pytree_node(
221
+ nodetype=new_cls, flatten_func=flatten, unflatten_func=unflatten
222
+ )
223
+ return new_cls
flax_model/alphafold3/jax/geometry/utils.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Utils for geometry library."""
4
+
5
+ from collections.abc import Iterable
6
+ import numbers
7
+
8
+ import jax
9
+ from jax import lax
10
+ import jax.numpy as jnp
11
+
12
+
13
+ def safe_select(condition, true_fn, false_fn):
14
+ """Safe version of selection (i.e. `where`).
15
+
16
+ This applies the double-where trick.
17
+ Like jnp.where, this function will still execute both branches and is
18
+ expected to be more lightweight than lax.cond. Other than NaN-semantics,
19
+ safe_select(condition, true_fn, false_fn) is equivalent to
20
+
21
+ jax.tree.map(lambda x, y: jnp.where(condition, x, y),
22
+ true_fn(),
23
+ false_fn()),
24
+
25
+ Compared to the naive implementation above, safe_select provides the
26
+ following guarantee: in either the forward or backward pass, a NaN produced
27
+ *during the execution of true_fn()* will not propagate to the rest of the
28
+ computation and similarly for false_fn. It is very important to note that
29
+ while true_fn and false_fn will typically close over other tensors (i.e. they
30
+ use values computed prior to the safe_select function), there is no NaN-safety
31
+ for the backward pass of closed over values. It is important than any NaN's
32
+ are produced within the branch functions and not before them. For example,
33
+
34
+ safe_select(x < eps, lambda: 0., lambda: jnp.sqrt(x))
35
+
36
+ will not produce NaN on the backward pass even if x == 0. since sqrt happens
37
+ within the false_fn, but the very similar
38
+
39
+ y = jnp.sqrt(x)
40
+ safe_select(x < eps, lambda: 0., lambda: y)
41
+
42
+ will produce a NaN on the backward pass if x == 0 because the sqrt happens
43
+ prior to the false_fn.
44
+
45
+ Args:
46
+ condition: Boolean array to use in where
47
+ true_fn: Zero-argument function to construct the values used in the True
48
+ condition. Tensors that this function closes over will be extracted
49
+ automatically to implement the double-where trick to suppress spurious NaN
50
+ propagation.
51
+ false_fn: False branch equivalent of true_fn
52
+
53
+ Returns:
54
+ Resulting PyTree equivalent to tree_map line above.
55
+ """
56
+ true_fn, true_args = jax.closure_convert(true_fn)
57
+ false_fn, false_args = jax.closure_convert(false_fn)
58
+
59
+ true_args = jax.tree.map(
60
+ lambda x: jnp.where(condition, x, lax.stop_gradient(x)), true_args
61
+ )
62
+
63
+ false_args = jax.tree.map(
64
+ lambda x: jnp.where(condition, lax.stop_gradient(x), x), false_args
65
+ )
66
+
67
+ return jax.tree.map(
68
+ lambda x, y: jnp.where(condition, x, y),
69
+ true_fn(*true_args),
70
+ false_fn(*false_args),
71
+ )
72
+
73
+
74
+ def unstack(value: jnp.ndarray, axis: int = -1) -> list[jnp.ndarray]:
75
+ return [
76
+ jnp.squeeze(v, axis=axis)
77
+ for v in jnp.split(value, value.shape[axis], axis=axis)
78
+ ]
79
+
80
+
81
+ def angdiff(alpha: jnp.ndarray, beta: jnp.ndarray) -> jnp.ndarray:
82
+ """Compute absolute difference between two angles."""
83
+ d = alpha - beta
84
+ d = (d + jnp.pi) % (2 * jnp.pi) - jnp.pi
85
+ return d
86
+
87
+
88
+ def safe_arctan2(
89
+ x1: jnp.ndarray, x2: jnp.ndarray, eps: float = 1e-8
90
+ ) -> jnp.ndarray:
91
+ """Safe version of arctan2 that avoids NaN gradients when x1=x2=0."""
92
+
93
+ return safe_select(
94
+ jnp.abs(x1) + jnp.abs(x2) < eps,
95
+ lambda: jnp.zeros_like(jnp.arctan2(x1, x2)),
96
+ lambda: jnp.arctan2(x1, x2),
97
+ )
98
+
99
+
100
+ def weighted_mean(
101
+ *,
102
+ weights: jnp.ndarray,
103
+ value: jnp.ndarray,
104
+ axis: int | Iterable[int] | None = None,
105
+ eps: float = 1e-10,
106
+ ) -> jnp.ndarray:
107
+ """Computes weighted mean in a safe way that avoids NaNs.
108
+
109
+ This is equivalent to jnp.average for the case eps=0.0, but adds a small
110
+ constant to the denominator of the weighted average to avoid NaNs.
111
+ 'weights' should be broadcastable to the shape of value.
112
+
113
+ Args:
114
+ weights: Weights to weight value by.
115
+ value: Values to average
116
+ axis: Axes to average over.
117
+ eps: Epsilon to add to the denominator.
118
+
119
+ Returns:
120
+ Weighted average.
121
+ """
122
+
123
+ weights = jnp.asarray(weights, dtype=value.dtype)
124
+ weights = jnp.broadcast_to(weights, value.shape)
125
+
126
+ weights_shape = weights.shape
127
+
128
+ if isinstance(axis, numbers.Integral):
129
+ axis = [axis]
130
+ elif axis is None:
131
+ axis = list(range(len(weights_shape)))
132
+
133
+ return jnp.sum(weights * value, axis=axis) / (
134
+ jnp.sum(weights, axis=axis) + eps
135
+ )
flax_model/alphafold3/jax/geometry/vector.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Vec3Array Class."""
4
+
5
+ import dataclasses
6
+ from typing import Final, Self, TypeAlias
7
+
8
+ from flax_model.alphafold3.jax.geometry import struct_of_array
9
+ from flax_model.alphafold3.jax.geometry import utils
10
+ import jax
11
+ import jax.numpy as jnp
12
+ import numpy as np
13
+
14
+
15
+ Float: TypeAlias = float | jnp.ndarray
16
+
17
+ VERSION: Final[str] = '0.1'
18
+
19
+
20
+ @struct_of_array.StructOfArray(same_dtype=True)
21
+ class Vec3Array:
22
+ """Vec3Array in 3 dimensional Space implemented as struct of arrays.
23
+
24
+ This is done in order to improve performance and precision.
25
+ On TPU small matrix multiplications are very suboptimal and will waste large
26
+ compute ressources, furthermore any matrix multiplication on TPU happens in
27
+ mixed bfloat16/float32 precision, which is often undesirable when handling
28
+ physical coordinates.
29
+
30
+ In most cases this will also be faster on CPUs/GPUs since it allows for easier
31
+ use of vector instructions.
32
+ """
33
+
34
+ x: jnp.ndarray = dataclasses.field(metadata={'dtype': jnp.float32})
35
+ y: jnp.ndarray
36
+ z: jnp.ndarray
37
+
38
+ def __post_init__(self):
39
+ if hasattr(self.x, 'dtype'):
40
+ if not self.x.dtype == self.y.dtype == self.z.dtype:
41
+ raise ValueError(
42
+ f'Type mismatch: {self.x.dtype}, {self.y.dtype}, {self.z.dtype}'
43
+ )
44
+ if not self.x.shape == self.y.shape == self.z.shape:
45
+ raise ValueError(
46
+ f'Shape mismatch: {self.x.shape}, {self.y.shape}, {self.z.shape}'
47
+ )
48
+
49
+ def __add__(self, other: Self) -> Self:
50
+ return jax.tree.map(lambda x, y: x + y, self, other)
51
+
52
+ def __sub__(self, other: Self) -> Self:
53
+ return jax.tree.map(lambda x, y: x - y, self, other)
54
+
55
+ def __mul__(self, other: Float) -> Self:
56
+ return jax.tree.map(lambda x: x * other, self)
57
+
58
+ def __rmul__(self, other: Float) -> Self:
59
+ return self * other
60
+
61
+ def __truediv__(self, other: Float) -> Self:
62
+ return jax.tree.map(lambda x: x / other, self)
63
+
64
+ def __neg__(self) -> Self:
65
+ return jax.tree.map(lambda x: -x, self)
66
+
67
+ def __pos__(self) -> Self:
68
+ return jax.tree.map(lambda x: x, self)
69
+
70
+ def cross(self, other: Self) -> Self:
71
+ """Compute cross product between 'self' and 'other'."""
72
+ new_x = self.y * other.z - self.z * other.y
73
+ new_y = self.z * other.x - self.x * other.z
74
+ new_z = self.x * other.y - self.y * other.x
75
+ return Vec3Array(new_x, new_y, new_z)
76
+
77
+ def dot(self, other: Self) -> Float:
78
+ """Compute dot product between 'self' and 'other'."""
79
+ return self.x * other.x + self.y * other.y + self.z * other.z
80
+
81
+ def norm(self, epsilon: float = 1e-6) -> Float:
82
+ """Compute Norm of Vec3Array, clipped to epsilon."""
83
+ # To avoid NaN on the backward pass, we must use maximum before the sqrt
84
+ norm2 = self.dot(self)
85
+ if epsilon:
86
+ norm2 = jnp.maximum(norm2, epsilon**2)
87
+ return jnp.sqrt(norm2)
88
+
89
+ def norm2(self):
90
+ return self.dot(self)
91
+
92
+ def normalized(self, epsilon: float = 1e-6) -> Self:
93
+ """Return unit vector with optional clipping."""
94
+ return self / self.norm(epsilon)
95
+
96
+ @classmethod
97
+ def zeros(cls, shape, dtype=jnp.float32):
98
+ """Return Vec3Array corresponding to zeros of given shape."""
99
+ return cls(
100
+ jnp.zeros(shape, dtype),
101
+ jnp.zeros(shape, dtype),
102
+ jnp.zeros(shape, dtype),
103
+ ) # pytype: disable=wrong-arg-count # trace-all-classes
104
+
105
+ def to_array(self) -> jnp.ndarray:
106
+ return jnp.stack([self.x, self.y, self.z], axis=-1)
107
+
108
+ @classmethod
109
+ def from_array(cls, array):
110
+ return cls(*utils.unstack(array))
111
+
112
+ def __getstate__(self):
113
+ return (
114
+ VERSION,
115
+ [np.asarray(self.x), np.asarray(self.y), np.asarray(self.z)],
116
+ )
117
+
118
+ def __setstate__(self, state):
119
+ version, state = state
120
+ del version
121
+ for i, letter in enumerate('xyz'):
122
+ object.__setattr__(self, letter, state[i])
123
+
124
+
125
+ def square_euclidean_distance(
126
+ vec1: Vec3Array, vec2: Vec3Array, epsilon: float = 1e-6
127
+ ) -> Float:
128
+ """Computes square of euclidean distance between 'vec1' and 'vec2'.
129
+
130
+ Args:
131
+ vec1: Vec3Array to compute distance to
132
+ vec2: Vec3Array to compute distance from, should be broadcast compatible
133
+ with 'vec1'
134
+ epsilon: distance is clipped from below to be at least epsilon
135
+
136
+ Returns:
137
+ Array of square euclidean distances;
138
+ shape will be result of broadcasting 'vec1' and 'vec2'
139
+ """
140
+ difference = vec1 - vec2
141
+ distance = difference.dot(difference)
142
+ if epsilon:
143
+ distance = jnp.maximum(distance, epsilon)
144
+ return distance
145
+
146
+
147
+ def dot(vector1: Vec3Array, vector2: Vec3Array) -> Float:
148
+ return vector1.dot(vector2)
149
+
150
+
151
+ def cross(vector1: Vec3Array, vector2: Vec3Array) -> Float:
152
+ return vector1.cross(vector2)
153
+
154
+
155
+ def norm(vector: Vec3Array, epsilon: float = 1e-6) -> Float:
156
+ return vector.norm(epsilon)
157
+
158
+
159
+ def normalized(vector: Vec3Array, epsilon: float = 1e-6) -> Vec3Array:
160
+ return vector.normalized(epsilon)
161
+
162
+
163
+ def euclidean_distance(
164
+ vec1: Vec3Array, vec2: Vec3Array, epsilon: float = 1e-6
165
+ ) -> Float:
166
+ """Computes euclidean distance between 'vec1' and 'vec2'.
167
+
168
+ Args:
169
+ vec1: Vec3Array to compute euclidean distance to
170
+ vec2: Vec3Array to compute euclidean distance from, should be broadcast
171
+ compatible with 'vec1'
172
+ epsilon: distance is clipped from below to be at least epsilon
173
+
174
+ Returns:
175
+ Array of euclidean distances;
176
+ shape will be result of broadcasting 'vec1' and 'vec2'
177
+ """
178
+ distance_sq = square_euclidean_distance(vec1, vec2, epsilon**2)
179
+ distance = jnp.sqrt(distance_sq)
180
+ return distance
181
+
182
+
183
+ def dihedral_angle(
184
+ a: Vec3Array, b: Vec3Array, c: Vec3Array, d: Vec3Array
185
+ ) -> Float:
186
+ """Computes torsion angle for a quadruple of points.
187
+
188
+ For points (a, b, c, d), this is the angle between the planes defined by
189
+ points (a, b, c) and (b, c, d). It is also known as the dihedral angle.
190
+
191
+ Arguments:
192
+ a: A Vec3Array of coordinates.
193
+ b: A Vec3Array of coordinates.
194
+ c: A Vec3Array of coordinates.
195
+ d: A Vec3Array of coordinates.
196
+
197
+ Returns:
198
+ A tensor of angles in radians: [-pi, pi].
199
+ """
200
+ v1 = a - b
201
+ v2 = b - c
202
+ v3 = d - c
203
+
204
+ c1 = v1.cross(v2)
205
+ c2 = v3.cross(v2)
206
+ c3 = c2.cross(c1)
207
+
208
+ v2_mag = v2.norm()
209
+ return jnp.arctan2(c3.dot(v2), v2_mag * c1.dot(c2))
210
+
211
+
212
+ def random_gaussian_vector(shape, key, dtype=jnp.float32) -> Vec3Array:
213
+ vec_array = jax.random.normal(key, shape + (3,), dtype)
214
+ return Vec3Array.from_array(vec_array)
flax_model/alphafold3/model/__init__.py ADDED
File without changes
flax_model/alphafold3/model/atom_layout/__init__.py ADDED
File without changes
flax_model/alphafold3/model/atom_layout/atom_layout.py ADDED
@@ -0,0 +1,1087 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Helper functions for different atom layouts and conversion between them."""
4
+
5
+ import collections
6
+ from collections.abc import Mapping, Sequence
7
+ import dataclasses
8
+ import types
9
+ from typing import Any, TypeAlias
10
+
11
+ from flax_model.alphafold3 import structure
12
+ from flax_model.alphafold3.constants import atom_types
13
+ from flax_model.alphafold3.constants import chemical_component_sets
14
+ from flax_model.alphafold3.constants import chemical_components
15
+ from flax_model.alphafold3.constants import mmcif_names
16
+ from flax_model.alphafold3.constants import residue_names
17
+ from flax_model.alphafold3.data.tools import rdkit_utils
18
+ from flax_model.alphafold3.structure import chemical_components as struc_chem_comps
19
+ import jax.numpy as jnp
20
+ import numpy as np
21
+ from rdkit import Chem
22
+
23
+
24
+ xnp_ndarray: TypeAlias = np.ndarray | jnp.ndarray # pylint: disable=invalid-name
25
+ NumpyIndex: TypeAlias = Any
26
+
27
+
28
+ @dataclasses.dataclass(frozen=True)
29
+ class AtomLayout:
30
+ """Atom layout in a fixed shape (usually 1-dim or 2-dim).
31
+
32
+ Examples for atom layouts are atom37, atom14, and similar.
33
+ All members are np.ndarrays with the same shape, e.g.
34
+ - [num_atoms]
35
+ - [num_residues, max_atoms_per_residue]
36
+ - [num_fragments, max_fragments_per_residue]
37
+ All string arrays should have dtype=object to avoid pitfalls with Numpy's
38
+ fixed-size strings
39
+
40
+ Attributes:
41
+ atom_name: np.ndarray of str: atom names (e.g. 'CA', 'NE2'), padding
42
+ elements have an empty string (''), None or any other value, that maps to
43
+ False for .astype(bool). mmCIF field: _atom_site.label_atom_id.
44
+ res_id: np.ndarray of int: residue index (usually starting from 1) padding
45
+ elements can have an arbitrary value. mmCIF field:
46
+ _atom_site.label_seq_id.
47
+ chain_id: np.ndarray of str: chain names (e.g. 'A', 'B') padding elements
48
+ can have an arbitrary value. mmCIF field: _atom_site.label_seq_id.
49
+ atom_element: np.ndarray of str: atom elements (e.g. 'C', 'N', 'O'), padding
50
+ elements have an empty string (''), None or any other value, that maps to
51
+ False for .astype(bool). mmCIF field: _atom_site.type_symbol.
52
+ res_name: np.ndarray of str: residue names (e.g. 'ARG', 'TRP') padding
53
+ elements can have an arbitrary value. mmCIF field:
54
+ _atom_site.label_comp_id.
55
+ chain_type: np.ndarray of str: chain types (e.g. 'polypeptide(L)'). padding
56
+ elements can have an arbitrary value. mmCIF field: _entity_poly.type OR
57
+ _entity.type (for non-polymers).
58
+ shape: shape of the layout (just returns atom_name.shape)
59
+ """
60
+
61
+ atom_name: np.ndarray
62
+ res_id: np.ndarray
63
+ chain_id: np.ndarray
64
+ atom_element: np.ndarray | None = None
65
+ res_name: np.ndarray | None = None
66
+ chain_type: np.ndarray | None = None
67
+
68
+ def __post_init__(self):
69
+ """Assert all arrays have the same shape."""
70
+ attribute_names = (
71
+ 'atom_name',
72
+ 'atom_element',
73
+ 'res_name',
74
+ 'res_id',
75
+ 'chain_id',
76
+ 'chain_type',
77
+ )
78
+ _assert_all_arrays_have_same_shape(
79
+ obj=self,
80
+ expected_shape=self.atom_name.shape,
81
+ attribute_names=attribute_names,
82
+ )
83
+ # atom_name must have dtype object, such that we can convert it to bool to
84
+ # obtain the mask
85
+ if self.atom_name.dtype != object:
86
+ raise ValueError(
87
+ 'atom_name must have dtype object, such that it can '
88
+ 'be converted converted to bool to obtain the mask'
89
+ )
90
+
91
+ def __getitem__(self, key: NumpyIndex) -> 'AtomLayout':
92
+ return AtomLayout(
93
+ atom_name=self.atom_name[key],
94
+ res_id=self.res_id[key],
95
+ chain_id=self.chain_id[key],
96
+ atom_element=(
97
+ self.atom_element[key] if self.atom_element is not None else None
98
+ ),
99
+ res_name=(self.res_name[key] if self.res_name is not None else None),
100
+ chain_type=(
101
+ self.chain_type[key] if self.chain_type is not None else None
102
+ ),
103
+ )
104
+
105
+ def __eq__(self, other: 'AtomLayout') -> bool:
106
+ if not np.array_equal(self.atom_name, other.atom_name):
107
+ return False
108
+
109
+ mask = self.atom_name.astype(bool)
110
+ # Check essential fields.
111
+ for field in ('res_id', 'chain_id'):
112
+ my_arr = getattr(self, field)
113
+ other_arr = getattr(other, field)
114
+ if not np.array_equal(my_arr[mask], other_arr[mask]):
115
+ return False
116
+
117
+ # Check optional fields.
118
+ for field in ('atom_element', 'res_name', 'chain_type'):
119
+ my_arr = getattr(self, field)
120
+ other_arr = getattr(other, field)
121
+ if (
122
+ my_arr is not None
123
+ and other_arr is not None
124
+ and not np.array_equal(my_arr[mask], other_arr[mask])
125
+ ):
126
+ return False
127
+
128
+ return True
129
+
130
+ def copy_and_pad_to(self, shape: tuple[int, ...]) -> 'AtomLayout':
131
+ """Copies and pads the layout to the requested shape.
132
+
133
+ Args:
134
+ shape: new shape for the atom layout
135
+
136
+ Returns:
137
+ a copy of the atom layout padded to the requested shape
138
+
139
+ Raises:
140
+ ValueError: incompatible shapes.
141
+ """
142
+ if len(shape) != len(self.atom_name.shape):
143
+ raise ValueError(
144
+ f'Incompatible shape {shape}. Current layout has shape {self.shape}.'
145
+ )
146
+ if any(new < old for old, new in zip(self.atom_name.shape, shape)):
147
+ raise ValueError(
148
+ "Can't pad to a smaller shape. Current layout has shape "
149
+ f'{self.shape} and you requested shape {shape}.'
150
+ )
151
+ pad_width = [
152
+ (0, new - old) for old, new in zip(self.atom_name.shape, shape)
153
+ ]
154
+ pad_val = np.array('', dtype=object)
155
+ return AtomLayout(
156
+ atom_name=np.pad(self.atom_name, pad_width, constant_values=pad_val),
157
+ res_id=np.pad(self.res_id, pad_width, constant_values=0),
158
+ chain_id=np.pad(self.chain_id, pad_width, constant_values=pad_val),
159
+ atom_element=(
160
+ np.pad(self.atom_element, pad_width, constant_values=pad_val)
161
+ if self.atom_element is not None
162
+ else None
163
+ ),
164
+ res_name=(
165
+ np.pad(self.res_name, pad_width, constant_values=pad_val)
166
+ if self.res_name is not None
167
+ else None
168
+ ),
169
+ chain_type=(
170
+ np.pad(self.chain_type, pad_width, constant_values=pad_val)
171
+ if self.chain_type is not None
172
+ else None
173
+ ),
174
+ )
175
+
176
+ def to_array(self) -> np.ndarray:
177
+ """Stacks the fields to a numpy array with shape (6, <layout_shape>).
178
+
179
+ Creates a pure numpy array of type `object` by stacking the 6 fields of the
180
+ AtomLayout, i.e. (atom_name, atom_element, res_name, res_id, chain_id,
181
+ chain_type). This method together with from_array() provides an easy way to
182
+ apply pure numpy methods like np.concatenate() to `AtomLayout`s.
183
+
184
+ Returns:
185
+ np.ndarray of object with shape (6, <layout_shape>), e.g.
186
+ array([['N', 'CA', 'C', ..., 'CB', 'CG', 'CD'],
187
+ ['N', 'C', 'C', ..., 'C', 'C', 'C'],
188
+ ['LEU', 'LEU', 'LEU', ..., 'PRO', 'PRO', 'PRO'],
189
+ [1, 1, 1, ..., 403, 403, 403],
190
+ ['A', 'A', 'A', ..., 'D', 'D', 'D'],
191
+ ['polypeptide(L)', 'polypeptide(L)', ..., 'polypeptide(L)']],
192
+ dtype=object)
193
+ """
194
+ if (
195
+ self.atom_element is None
196
+ or self.res_name is None
197
+ or self.chain_type is None
198
+ ):
199
+ raise ValueError('All optional fields need to be present.')
200
+
201
+ return np.stack(dataclasses.astuple(self), axis=0)
202
+
203
+ @classmethod
204
+ def from_array(cls, arr: np.ndarray) -> 'AtomLayout':
205
+ """Creates an AtomLayout object from a numpy array with shape (6, ...).
206
+
207
+ see also to_array()
208
+ Args:
209
+ arr: np.ndarray of object with shape (6, <layout_shape>)
210
+
211
+ Returns:
212
+ AtomLayout object with shape (<layout_shape>)
213
+ """
214
+ if arr.shape[0] != 6:
215
+ raise ValueError(
216
+ 'Given array must have shape (6, ...) to match the 6 fields of '
217
+ 'AtomLayout (atom_name, atom_element, res_name, res_id, chain_id, '
218
+ f'chain_type). Your array has {arr.shape=}'
219
+ )
220
+ return cls(*arr)
221
+
222
+ @property
223
+ def shape(self) -> tuple[int, ...]:
224
+ return self.atom_name.shape
225
+
226
+
227
+ @dataclasses.dataclass(frozen=True)
228
+ class Residues:
229
+ """List of residues with meta data.
230
+
231
+ Attributes:
232
+ res_name: np.ndarray of str [num_res], e.g. 'ARG', 'TRP'
233
+ res_id: np.ndarray of int [num_res]
234
+ chain_id: np.ndarray of str [num_res], e.g. 'A', 'B'
235
+ chain_type: np.ndarray of str [num_res], e.g. 'polypeptide(L)'
236
+ is_start_terminus: np.ndarray of bool [num_res]
237
+ is_end_terminus: np.ndarray of bool [num_res]
238
+ deprotonation: (optional) np.ndarray of set() [num_res], e.g. {'HD1', 'HE2'}
239
+ smiles_string: (optional) np.ndarray of str [num_res], e.g. 'Cc1ccccc1'
240
+ shape: shape of the layout (just returns res_name.shape)
241
+ """
242
+
243
+ res_name: np.ndarray
244
+ res_id: np.ndarray
245
+ chain_id: np.ndarray
246
+ chain_type: np.ndarray
247
+ is_start_terminus: np.ndarray
248
+ is_end_terminus: np.ndarray
249
+ deprotonation: np.ndarray | None = None
250
+ smiles_string: np.ndarray | None = None
251
+
252
+ def __post_init__(self):
253
+ """Assert all arrays are 1D have the same shape."""
254
+ attribute_names = (
255
+ 'res_name',
256
+ 'res_id',
257
+ 'chain_id',
258
+ 'chain_type',
259
+ 'is_start_terminus',
260
+ 'is_end_terminus',
261
+ 'deprotonation',
262
+ 'smiles_string',
263
+ )
264
+ _assert_all_arrays_have_same_shape(
265
+ obj=self,
266
+ expected_shape=(self.res_name.shape[0],),
267
+ attribute_names=attribute_names,
268
+ )
269
+
270
+ def __getitem__(self, key: NumpyIndex) -> 'Residues':
271
+ return Residues(
272
+ res_name=self.res_name[key],
273
+ res_id=self.res_id[key],
274
+ chain_id=self.chain_id[key],
275
+ chain_type=self.chain_type[key],
276
+ is_start_terminus=self.is_start_terminus[key],
277
+ is_end_terminus=self.is_end_terminus[key],
278
+ deprotonation=(
279
+ self.deprotonation[key] if self.deprotonation is not None else None
280
+ ),
281
+ smiles_string=(
282
+ self.smiles_string[key] if self.smiles_string is not None else None
283
+ ),
284
+ )
285
+
286
+ def __eq__(self, other: 'Residues') -> bool:
287
+ return all(
288
+ np.array_equal(getattr(self, field.name), getattr(other, field.name))
289
+ for field in dataclasses.fields(self)
290
+ )
291
+
292
+ @property
293
+ def shape(self) -> tuple[int, ...]:
294
+ return self.res_name.shape
295
+
296
+
297
+ @dataclasses.dataclass(frozen=True)
298
+ class GatherInfo:
299
+ """Gather indices to translate from one atom layout to another.
300
+
301
+ All members are np or jnp ndarray (usually 1-dim or 2-dim) with the same
302
+ shape, e.g.
303
+ - [num_atoms]
304
+ - [num_residues, max_atoms_per_residue]
305
+ - [num_fragments, max_fragments_per_residue]
306
+
307
+ Attributes:
308
+ gather_idxs: np or jnp ndarray of int: gather indices into a flattened array
309
+ gather_mask: np or jnp ndarray of bool: mask for resulting array
310
+ input_shape: np or jnp ndarray of int: the shape of the unflattened input
311
+ array
312
+ shape: output shape. Just returns gather_idxs.shape
313
+ """
314
+
315
+ gather_idxs: xnp_ndarray
316
+ gather_mask: xnp_ndarray
317
+ input_shape: xnp_ndarray
318
+
319
+ def __post_init__(self):
320
+ if self.gather_mask.shape != self.gather_idxs.shape:
321
+ raise ValueError(
322
+ 'All arrays must have the same shape. Got\n'
323
+ f'gather_idxs.shape = {self.gather_idxs.shape}\n'
324
+ f'gather_mask.shape = {self.gather_mask.shape}\n'
325
+ )
326
+
327
+ def __getitem__(self, key: NumpyIndex) -> 'GatherInfo':
328
+ return GatherInfo(
329
+ gather_idxs=self.gather_idxs[key],
330
+ gather_mask=self.gather_mask[key],
331
+ input_shape=self.input_shape,
332
+ )
333
+
334
+ @property
335
+ def shape(self) -> tuple[int, ...]:
336
+ return self.gather_idxs.shape
337
+
338
+ def as_np_or_jnp(self, xnp: types.ModuleType) -> 'GatherInfo':
339
+ return GatherInfo(
340
+ gather_idxs=xnp.array(self.gather_idxs),
341
+ gather_mask=xnp.array(self.gather_mask),
342
+ input_shape=xnp.array(self.input_shape),
343
+ )
344
+
345
+ def as_dict(
346
+ self,
347
+ key_prefix: str | None = None,
348
+ ) -> dict[str, xnp_ndarray]:
349
+ prefix = f'{key_prefix}:' if key_prefix else ''
350
+ return {
351
+ prefix + 'gather_idxs': self.gather_idxs,
352
+ prefix + 'gather_mask': self.gather_mask,
353
+ prefix + 'input_shape': self.input_shape,
354
+ }
355
+
356
+ @classmethod
357
+ def from_dict(
358
+ cls,
359
+ d: Mapping[str, xnp_ndarray],
360
+ key_prefix: str | None = None,
361
+ ) -> 'GatherInfo':
362
+ """Creates GatherInfo from a given dictionary."""
363
+ prefix = f'{key_prefix}:' if key_prefix else ''
364
+ return cls(
365
+ gather_idxs=d[prefix + 'gather_idxs'],
366
+ gather_mask=d[prefix + 'gather_mask'],
367
+ input_shape=d[prefix + 'input_shape'],
368
+ )
369
+
370
+
371
+ def fill_in_optional_fields(
372
+ minimal_atom_layout: AtomLayout,
373
+ reference_atoms: AtomLayout,
374
+ ) -> AtomLayout:
375
+ """Fill in the optional fields (atom_element, res_name, chain_type).
376
+
377
+ Extracts the optional fields (atom_element, res_name, chain_type) from a
378
+ flat reference layout and fills them into the fields from this layout.
379
+
380
+ Args:
381
+ minimal_atom_layout: An AtomLayout that only contains the essential fields
382
+ (atom_name, res_id, chain_id).
383
+ reference_atoms: A flat layout that contains all fields for all atoms.
384
+
385
+ Returns:
386
+ An AtomLayout that contains all fields.
387
+
388
+ Raises:
389
+ ValueError: Reference atoms layout is not flat.
390
+ ValueError: Missing atoms in reference.
391
+ """
392
+ if len(reference_atoms.shape) > 1:
393
+ raise ValueError('Only flat layouts are supported as reference.')
394
+ ref_to_self = compute_gather_idxs(
395
+ source_layout=reference_atoms, target_layout=minimal_atom_layout
396
+ )
397
+ atom_mask = minimal_atom_layout.atom_name.astype(bool)
398
+ missing_atoms_mask = atom_mask & ~ref_to_self.gather_mask
399
+ if np.any(missing_atoms_mask):
400
+ raise ValueError(
401
+ f'{np.sum(missing_atoms_mask)} missing atoms in reference: '
402
+ f'{minimal_atom_layout[missing_atoms_mask]}'
403
+ )
404
+
405
+ def _convert_str_array(gather: GatherInfo, arr: np.ndarray):
406
+ output = arr[gather.gather_idxs]
407
+ output[~gather.gather_mask] = ''
408
+ return output
409
+
410
+ return dataclasses.replace(
411
+ minimal_atom_layout,
412
+ atom_element=_convert_str_array(
413
+ ref_to_self, reference_atoms.atom_element
414
+ ),
415
+ res_name=_convert_str_array(ref_to_self, reference_atoms.res_name),
416
+ chain_type=_convert_str_array(ref_to_self, reference_atoms.chain_type),
417
+ )
418
+
419
+
420
+ def guess_deprotonation(residues: Residues) -> Residues:
421
+ """Convenience function to create a plausible deprotonation field.
422
+
423
+ Assumes a pH of 7 and always prefers HE2 over HD1 for HIS.
424
+ Args:
425
+ residues: a Residues object without a depronotation field
426
+
427
+ Returns:
428
+ a Residues object with a depronotation field
429
+ """
430
+ num_residues = residues.res_name.shape[0]
431
+ deprotonation = np.empty(num_residues, dtype=object)
432
+ deprotonation_at_ph7 = {
433
+ 'ASP': 'HD2',
434
+ 'GLU': 'HE2',
435
+ 'HIS': 'HD1',
436
+ }
437
+ for idx, res_name in enumerate(residues.res_name):
438
+ deprotonation[idx] = set()
439
+ if res_name in deprotonation_at_ph7:
440
+ deprotonation[idx].add(deprotonation_at_ph7[res_name])
441
+ if residues.is_end_terminus[idx]:
442
+ deprotonation[idx].add('HXT')
443
+
444
+ return dataclasses.replace(residues, deprotonation=deprotonation)
445
+
446
+
447
+ def atom_layout_from_structure(
448
+ struct: structure.Structure,
449
+ *,
450
+ fix_non_standard_polymer_res: bool = False,
451
+ ) -> AtomLayout:
452
+ """Extract AtomLayout from a Structure."""
453
+
454
+ if not fix_non_standard_polymer_res:
455
+ return AtomLayout(
456
+ atom_name=np.array(struct.atom_name, dtype=object),
457
+ atom_element=np.array(struct.atom_element, dtype=object),
458
+ res_name=np.array(struct.res_name, dtype=object),
459
+ res_id=np.array(struct.res_id, dtype=int),
460
+ chain_id=np.array(struct.chain_id, dtype=object),
461
+ chain_type=np.array(struct.chain_type, dtype=object),
462
+ )
463
+
464
+ # Target lists.
465
+ target_atom_names = []
466
+ target_atom_elements = []
467
+ target_res_ids = []
468
+ target_res_names = []
469
+ target_chain_ids = []
470
+ target_chain_types = []
471
+
472
+ for atom in struct.iter_atoms():
473
+ target_atom_names.append(atom['atom_name'])
474
+ target_atom_elements.append(atom['atom_element'])
475
+ target_res_ids.append(atom['res_id'])
476
+ target_chain_ids.append(atom['chain_id'])
477
+ target_chain_types.append(atom['chain_type'])
478
+ if mmcif_names.is_standard_polymer_type(atom['chain_type']):
479
+ fixed_res_name = mmcif_names.fix_non_standard_polymer_res(
480
+ res_name=atom['res_name'], chain_type=atom['chain_type']
481
+ )
482
+ target_res_names.append(fixed_res_name)
483
+ else:
484
+ target_res_names.append(atom['res_name'])
485
+
486
+ return AtomLayout(
487
+ atom_name=np.array(target_atom_names, dtype=object),
488
+ atom_element=np.array(target_atom_elements, dtype=object),
489
+ res_name=np.array(target_res_names, dtype=object),
490
+ res_id=np.array(target_res_ids, dtype=int),
491
+ chain_id=np.array(target_chain_ids, dtype=object),
492
+ chain_type=np.array(target_chain_types, dtype=object),
493
+ )
494
+
495
+
496
+ def residues_from_structure(
497
+ struct: structure.Structure,
498
+ *,
499
+ include_missing_residues: bool = True,
500
+ fix_non_standard_polymer_res: bool = False,
501
+ ) -> Residues:
502
+ """Create a Residues object from a Structure object."""
503
+
504
+ def _get_smiles(res_name):
505
+ """Get SMILES string from chemical components."""
506
+ smiles = None
507
+ if (
508
+ struct.chemical_components_data is not None
509
+ and struct.chemical_components_data.chem_comp is not None
510
+ and struct.chemical_components_data.chem_comp.get(res_name)
511
+ ):
512
+ smiles = struct.chemical_components_data.chem_comp[res_name].pdbx_smiles
513
+ return smiles
514
+
515
+ res_names_per_chain = struct.chain_res_name_sequence(
516
+ include_missing_residues=include_missing_residues,
517
+ fix_non_standard_polymer_res=fix_non_standard_polymer_res,
518
+ )
519
+ res_name = []
520
+ res_id = []
521
+ chain_id = []
522
+ chain_type = []
523
+ smiles = []
524
+ is_start_terminus = []
525
+ for c in struct.iter_chains():
526
+ if include_missing_residues:
527
+ this_res_ids = [id for (_, id) in struct.all_residues[c['chain_id']]]
528
+ else:
529
+ this_res_ids = [
530
+ r['res_id']
531
+ for r in struct.iter_residues()
532
+ if r['chain_id'] == c['chain_id']
533
+ ]
534
+ fixed_res_names = res_names_per_chain[c['chain_id']]
535
+ assert len(this_res_ids) == len(
536
+ fixed_res_names
537
+ ), f'{len(this_res_ids)} != {len(fixed_res_names)}'
538
+ this_start_res_id = min(min(this_res_ids), 1)
539
+ this_is_start_terminus = [r == this_start_res_id for r in this_res_ids]
540
+ smiles.extend([_get_smiles(res_name) for res_name in fixed_res_names])
541
+ num_res = len(fixed_res_names)
542
+ res_name.extend(fixed_res_names)
543
+ res_id.extend(this_res_ids)
544
+ chain_id.extend([c['chain_id']] * num_res)
545
+ chain_type.extend([c['chain_type']] * num_res)
546
+ is_start_terminus.extend(this_is_start_terminus)
547
+ res_name = np.array(res_name, dtype=object)
548
+ res_id = np.array(res_id, dtype=int)
549
+ chain_id = np.array(chain_id, dtype=object)
550
+ chain_type = np.array(chain_type, dtype=object)
551
+ smiles = np.array(smiles, dtype=object)
552
+ is_start_terminus = np.array(is_start_terminus, dtype=bool)
553
+
554
+ res_uid_to_idx = {
555
+ uid: idx for idx, uid in enumerate(zip(chain_id, res_id, strict=True))
556
+ }
557
+
558
+ # Start terminus indicates whether residue index is 1 and chain is polymer.
559
+ is_polymer = np.isin(chain_type, tuple(mmcif_names.POLYMER_CHAIN_TYPES))
560
+ is_start_terminus = is_start_terminus & is_polymer
561
+
562
+ # Start also indicates whether amino acid is attached to H2 or proline to H.
563
+ start_terminus_atom_index = np.nonzero(
564
+ (struct.chain_type == mmcif_names.PROTEIN_CHAIN)
565
+ & (
566
+ (struct.atom_name == 'H2')
567
+ | ((struct.atom_name == 'H') & (struct.res_name == 'PRO'))
568
+ )
569
+ )[0]
570
+
571
+ # Translate atom idx to residue idx to assign start terminus.
572
+ for atom_idx in start_terminus_atom_index:
573
+ res_uid = (struct.chain_id[atom_idx], struct.res_id[atom_idx])
574
+ res_idx = res_uid_to_idx[res_uid]
575
+ is_start_terminus[res_idx] = True
576
+
577
+ # Infer end terminus: Check for OXT, or in case of
578
+ # include_missing_residues==True for the last residue of the chain.
579
+ num_all_residues = res_name.shape[0]
580
+ is_end_terminus = np.zeros(num_all_residues, dtype=bool)
581
+ end_term_atom_idxs = np.nonzero(struct.atom_name == 'OXT')[0]
582
+ for atom_idx in end_term_atom_idxs:
583
+ res_uid = (struct.chain_id[atom_idx], struct.res_id[atom_idx])
584
+ res_idx = res_uid_to_idx[res_uid]
585
+ is_end_terminus[res_idx] = True
586
+
587
+ if include_missing_residues:
588
+ for idx in range(num_all_residues - 1):
589
+ if is_polymer[idx] and chain_id[idx] != chain_id[idx + 1]:
590
+ is_end_terminus[idx] = True
591
+ if (num_all_residues > 0) and is_polymer[-1]:
592
+ is_end_terminus[-1] = True
593
+
594
+ # Infer (de-)protonation: Only if hydrogens are given.
595
+ num_hydrogens = np.sum(
596
+ (struct.atom_element == 'H') & (struct.chain_type == 'polypeptide(L)')
597
+ )
598
+ if num_hydrogens > 0:
599
+ deprotonation = np.empty(num_all_residues, dtype=object)
600
+ all_atom_uids = set(
601
+ zip(struct.chain_id, struct.res_id, struct.atom_name, strict=True)
602
+ )
603
+ for idx in range(num_all_residues):
604
+ deprotonation[idx] = set()
605
+ check_hydrogens = set()
606
+ if is_end_terminus[idx]:
607
+ check_hydrogens.add('HXT')
608
+ if res_name[idx] in atom_types.PROTONATION_HYDROGENS:
609
+ check_hydrogens.update(atom_types.PROTONATION_HYDROGENS[res_name[idx]])
610
+ for hydrogen in check_hydrogens:
611
+ if (chain_id[idx], res_id[idx], hydrogen) not in all_atom_uids:
612
+ deprotonation[idx].add(hydrogen)
613
+ else:
614
+ deprotonation = None
615
+
616
+ return Residues(
617
+ res_name=res_name,
618
+ res_id=res_id,
619
+ chain_id=chain_id,
620
+ chain_type=chain_type,
621
+ is_start_terminus=is_start_terminus.astype(bool),
622
+ is_end_terminus=is_end_terminus,
623
+ deprotonation=deprotonation,
624
+ smiles_string=smiles,
625
+ )
626
+
627
+
628
+ def get_link_drop_atoms(
629
+ res_name: str,
630
+ chain_type: str,
631
+ *,
632
+ is_start_terminus: bool,
633
+ is_end_terminus: bool,
634
+ bonded_atoms: set[str],
635
+ drop_ligand_leaving_atoms: bool = False,
636
+ ) -> set[str]:
637
+ """Returns set of atoms that are dropped when this res_name gets linked.
638
+
639
+ Args:
640
+ res_name: residue name, e.g. 'ARG'
641
+ chain_type: chain_type, e.g. 'polypeptide(L)'
642
+ is_start_terminus: whether the residue is the n-terminus
643
+ is_end_terminus: whether the residue is the c-terminus
644
+ bonded_atoms: Names of atoms coming off this residue.
645
+ drop_ligand_leaving_atoms: Flag to switch on/off leaving atoms for ligands.
646
+
647
+ Returns:
648
+ Set of atoms that are dropped when this amino acid gets linked.
649
+ """
650
+ drop_atoms = set()
651
+ if chain_type == mmcif_names.PROTEIN_CHAIN:
652
+ if res_name == 'PRO':
653
+ if not is_start_terminus:
654
+ drop_atoms.update({'H', 'H2', 'H3'})
655
+ if not is_end_terminus:
656
+ drop_atoms.update({'OXT', 'HXT'})
657
+ else:
658
+ if not is_start_terminus:
659
+ drop_atoms.update({'H2', 'H3'})
660
+ if not is_end_terminus:
661
+ drop_atoms.update({'OXT', 'HXT'})
662
+ elif chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES:
663
+ if not is_start_terminus:
664
+ drop_atoms.update({'OP3'})
665
+ elif (
666
+ drop_ligand_leaving_atoms and chain_type in mmcif_names.LIGAND_CHAIN_TYPES
667
+ ):
668
+ if res_name in {
669
+ *chemical_component_sets.GLYCAN_OTHER_LIGANDS,
670
+ *chemical_component_sets.GLYCAN_LINKING_LIGANDS,
671
+ }:
672
+ if 'O1' not in bonded_atoms:
673
+ drop_atoms.update({'O1'})
674
+ return drop_atoms
675
+
676
+
677
+ def get_bonded_atoms(
678
+ polymer_ligand_bonds: AtomLayout,
679
+ ligand_ligand_bonds: AtomLayout,
680
+ res_id: int,
681
+ chain_id: str,
682
+ ) -> set[str]:
683
+ """Finds the res_name on the opposite end of the bond, if a bond exists.
684
+
685
+ Args:
686
+ polymer_ligand_bonds: Bond information for polymer-ligand pairs.
687
+ ligand_ligand_bonds: Bond information for ligand-ligand pairs.
688
+ res_id: residue id in question.
689
+ chain_id: chain id of residue in question.
690
+
691
+ Returns:
692
+ res_name of bonded atom.
693
+ """
694
+ bonded_atoms = set()
695
+ if polymer_ligand_bonds:
696
+ # Filter before searching to speed this up.
697
+ bond_idx = np.logical_and(
698
+ polymer_ligand_bonds.res_id == res_id,
699
+ polymer_ligand_bonds.chain_id == chain_id,
700
+ ).any(axis=1)
701
+ relevant_polymer_bonds = polymer_ligand_bonds[bond_idx]
702
+ for atom_names, res_ids, chain_ids in zip(
703
+ relevant_polymer_bonds.atom_name,
704
+ relevant_polymer_bonds.res_id,
705
+ relevant_polymer_bonds.chain_id,
706
+ ):
707
+ if (res_ids[0], chain_ids[0]) == (res_id, chain_id):
708
+ bonded_atoms.add(atom_names[0])
709
+ elif (res_ids[1], chain_ids[1]) == (res_id, chain_id):
710
+ bonded_atoms.add(atom_names[1])
711
+ if ligand_ligand_bonds:
712
+ bond_idx = np.logical_and(
713
+ ligand_ligand_bonds.res_id == res_id,
714
+ ligand_ligand_bonds.chain_id == chain_id,
715
+ ).any(axis=1)
716
+ relevant_ligand_bonds = ligand_ligand_bonds[bond_idx]
717
+ for atom_names, res_ids, chain_ids in zip(
718
+ relevant_ligand_bonds.atom_name,
719
+ relevant_ligand_bonds.res_id,
720
+ relevant_ligand_bonds.chain_id,
721
+ ):
722
+ if (res_ids[0], chain_ids[0]) == (res_id, chain_id):
723
+ bonded_atoms.add(atom_names[0])
724
+ elif (res_ids[1], chain_ids[1]) == (res_id, chain_id):
725
+ bonded_atoms.add(atom_names[1])
726
+ return bonded_atoms
727
+
728
+
729
+ def make_flat_atom_layout(
730
+ residues: Residues,
731
+ ccd: chemical_components.Ccd,
732
+ polymer_ligand_bonds: AtomLayout | None = None,
733
+ ligand_ligand_bonds: AtomLayout | None = None,
734
+ *,
735
+ with_hydrogens: bool = False,
736
+ skip_unk_residues: bool = True,
737
+ drop_ligand_leaving_atoms: bool = False,
738
+ ) -> AtomLayout:
739
+ """Make a flat atom layout for given residues.
740
+
741
+ Create a flat layout from a `Residues` object. The required atoms for each
742
+ amino acid type are taken from the CCD, hydrogens and oxygens are dropped to
743
+ make the linked residues. Terminal OXT's and protonation state for the
744
+ hydrogens come from the `Residues` object.
745
+
746
+ Args:
747
+ residues: a `Residues` object.
748
+ ccd: The chemical components dictionary.
749
+ polymer_ligand_bonds: Bond information for polymer-ligand pairs.
750
+ ligand_ligand_bonds: Bond information for ligand-ligand pairs.
751
+ with_hydrogens: whether to create hydrogens
752
+ skip_unk_residues: whether to skip 'UNK' resides -- default is True to be
753
+ compatible with the rest of AlphaFold that does not predict atoms for
754
+ unknown residues
755
+ drop_ligand_leaving_atoms: Flag to switch on/ off leaving atoms for ligands.
756
+
757
+ Returns:
758
+ an `AtomLayout` object
759
+ """
760
+ num_res = residues.res_name.shape[0]
761
+
762
+ # Target lists.
763
+ target_atom_names = []
764
+ target_atom_elements = []
765
+ target_res_ids = []
766
+ target_res_names = []
767
+ target_chain_ids = []
768
+ target_chain_types = []
769
+
770
+ for idx in range(num_res):
771
+ # skip 'UNK' residues if requested
772
+ if (
773
+ skip_unk_residues
774
+ and residues.res_name[idx] in residue_names.UNKNOWN_TYPES
775
+ ):
776
+ continue
777
+
778
+ # Get the atoms for this residue type from CCD.
779
+ if ccd.get(residues.res_name[idx]):
780
+ res_atoms = struc_chem_comps.get_all_atoms_in_entry(
781
+ ccd=ccd, res_name=residues.res_name[idx]
782
+ )
783
+ atom_names_elements = list(
784
+ zip(
785
+ res_atoms['_chem_comp_atom.atom_id'],
786
+ res_atoms['_chem_comp_atom.type_symbol'],
787
+ strict=True,
788
+ )
789
+ )
790
+ elif residues.smiles_string[idx]:
791
+ # Get atoms from RDKit via SMILES.
792
+ mol = Chem.MolFromSmiles(residues.smiles_string[idx])
793
+ if mol is None:
794
+ raise ValueError(
795
+ f'Failed to construct RDKit Mol for {residues.res_name[idx]} from'
796
+ f' SMILES string: {residues.smiles_string[idx]} . This is likely'
797
+ ' due to an issue with the SMILES string. Note that the userCCD'
798
+ ' input format provides an alternative way to define custom'
799
+ ' molecules directly without RDKit or SMILES.'
800
+ )
801
+ mol = rdkit_utils.assign_atom_names_from_graph(mol)
802
+ atom_names_elements = [
803
+ (a.GetProp('atom_name'), a.GetSymbol()) for a in mol.GetAtoms()
804
+ ]
805
+ else:
806
+ raise ValueError(
807
+ f'{residues.res_name[idx]} not found in CCD and no SMILES string'
808
+ )
809
+
810
+ # Remove hydrogens if requested.
811
+ if not with_hydrogens:
812
+ atom_names_elements = [
813
+ (n, e) for n, e in atom_names_elements if (e != 'H' and e != 'D')
814
+ ]
815
+ bonded_atoms = get_bonded_atoms(
816
+ polymer_ligand_bonds,
817
+ ligand_ligand_bonds,
818
+ residues.res_id[idx],
819
+ residues.chain_id[idx],
820
+ )
821
+ # Connect the amino-acids, i.e. remove OXT, HXT and H2.
822
+ drop_atoms = get_link_drop_atoms(
823
+ res_name=residues.res_name[idx],
824
+ chain_type=residues.chain_type[idx],
825
+ is_start_terminus=residues.is_start_terminus[idx],
826
+ is_end_terminus=residues.is_end_terminus[idx],
827
+ bonded_atoms=bonded_atoms,
828
+ drop_ligand_leaving_atoms=drop_ligand_leaving_atoms,
829
+ )
830
+
831
+ # If deprotonation info is available, remove the specific atoms.
832
+ if residues.deprotonation is not None:
833
+ drop_atoms.update(residues.deprotonation[idx])
834
+
835
+ atom_names_elements = [
836
+ (n, e) for n, e in atom_names_elements if n not in drop_atoms
837
+ ]
838
+
839
+ # Append the found atoms to the target lists.
840
+ target_atom_names.extend([n for n, _ in atom_names_elements])
841
+ target_atom_elements.extend([e for _, e in atom_names_elements])
842
+ num_atoms = len(atom_names_elements)
843
+ target_res_names.extend([residues.res_name[idx]] * num_atoms)
844
+ target_res_ids.extend([residues.res_id[idx]] * num_atoms)
845
+ target_chain_ids.extend([residues.chain_id[idx]] * num_atoms)
846
+ target_chain_types.extend([residues.chain_type[idx]] * num_atoms)
847
+
848
+ return AtomLayout(
849
+ atom_name=np.array(target_atom_names, dtype=object),
850
+ atom_element=np.array(target_atom_elements, dtype=object),
851
+ res_name=np.array(target_res_names, dtype=object),
852
+ res_id=np.array(target_res_ids, dtype=int),
853
+ chain_id=np.array(target_chain_ids, dtype=object),
854
+ chain_type=np.array(target_chain_types, dtype=object),
855
+ )
856
+
857
+
858
+ def compute_gather_idxs(
859
+ *,
860
+ source_layout: AtomLayout,
861
+ target_layout: AtomLayout,
862
+ fill_value: int = 0,
863
+ ) -> GatherInfo:
864
+ """Produce gather indices and mask to convert from source layout to target."""
865
+ source_uid_to_idx = {
866
+ uid: idx
867
+ for idx, uid in enumerate(
868
+ zip(
869
+ source_layout.chain_id.ravel(),
870
+ source_layout.res_id.ravel(),
871
+ source_layout.atom_name.ravel(),
872
+ strict=True,
873
+ )
874
+ )
875
+ }
876
+ gather_idxs = []
877
+ gather_mask = []
878
+ for uid in zip(
879
+ target_layout.chain_id.ravel(),
880
+ target_layout.res_id.ravel(),
881
+ target_layout.atom_name.ravel(),
882
+ strict=True,
883
+ ):
884
+ if uid in source_uid_to_idx:
885
+ gather_idxs.append(source_uid_to_idx[uid])
886
+ gather_mask.append(True)
887
+ else:
888
+ gather_idxs.append(fill_value)
889
+ gather_mask.append(False)
890
+ target_shape = target_layout.atom_name.shape
891
+ return GatherInfo(
892
+ gather_idxs=np.array(gather_idxs, dtype=int).reshape(target_shape),
893
+ gather_mask=np.array(gather_mask, dtype=bool).reshape(target_shape),
894
+ input_shape=np.array(source_layout.atom_name.shape),
895
+ )
896
+
897
+
898
+ def convert(
899
+ gather_info: GatherInfo,
900
+ arr: xnp_ndarray,
901
+ *,
902
+ layout_axes: tuple[int, ...] = (0,),
903
+ ) -> xnp_ndarray:
904
+ """Convert an array from one atom layout to another."""
905
+ # Translate negative indices to the corresponding positives.
906
+ layout_axes = tuple(i if i >= 0 else i + arr.ndim for i in layout_axes)
907
+
908
+ # Ensure that layout_axes are continuous.
909
+ layout_axes_begin = layout_axes[0]
910
+ layout_axes_end = layout_axes[-1] + 1
911
+
912
+ if layout_axes != tuple(range(layout_axes_begin, layout_axes_end)):
913
+ raise ValueError(f'layout_axes must be continuous. Got {layout_axes}.')
914
+ layout_shape = arr.shape[layout_axes_begin:layout_axes_end]
915
+
916
+ # Ensure that the layout shape is compatible
917
+ # with the gather_info. I.e. the first axis size must be equal or greater
918
+ # than the gather_info.input_shape, and all subsequent axes sizes must match.
919
+ if (len(layout_shape) != gather_info.input_shape.size) or (
920
+ isinstance(gather_info.input_shape, np.ndarray)
921
+ and (
922
+ (layout_shape[0] < gather_info.input_shape[0])
923
+ or (np.any(layout_shape[1:] != gather_info.input_shape[1:]))
924
+ )
925
+ ):
926
+ raise ValueError(
927
+ 'Input array layout axes are incompatible. You specified layout '
928
+ f'axes {layout_axes} with an input array of shape {arr.shape}, but '
929
+ f'the gather info expects shape {gather_info.input_shape}. '
930
+ 'Your first axis size must be equal or greater than the '
931
+ 'gather_info.input_shape, and all subsequent axes sizes must '
932
+ 'match.'
933
+ )
934
+
935
+ # Compute the shape of the input array with flattened layout.
936
+ batch_shape = arr.shape[:layout_axes_begin]
937
+ features_shape = arr.shape[layout_axes_end:]
938
+ arr_flattened_shape = batch_shape + (np.prod(layout_shape),) + features_shape
939
+
940
+ # Flatten input array and perform the gather.
941
+ arr_flattened = arr.reshape(arr_flattened_shape)
942
+ if layout_axes_begin == 0:
943
+ out_arr = arr_flattened[gather_info.gather_idxs, ...]
944
+ elif layout_axes_begin == 1:
945
+ out_arr = arr_flattened[:, gather_info.gather_idxs, ...]
946
+ elif layout_axes_begin == 2:
947
+ out_arr = arr_flattened[:, :, gather_info.gather_idxs, ...]
948
+ elif layout_axes_begin == 3:
949
+ out_arr = arr_flattened[:, :, :, gather_info.gather_idxs, ...]
950
+ elif layout_axes_begin == 4:
951
+ out_arr = arr_flattened[:, :, :, :, gather_info.gather_idxs, ...]
952
+ else:
953
+ raise ValueError(
954
+ 'Only 4 batch axes supported. If you need more, the code '
955
+ 'is easy to extend.'
956
+ )
957
+
958
+ # Broadcast the mask and apply it.
959
+ broadcasted_mask_shape = (
960
+ (1,) * len(batch_shape)
961
+ + gather_info.gather_mask.shape
962
+ + (1,) * len(features_shape)
963
+ )
964
+ out_arr *= gather_info.gather_mask.reshape(broadcasted_mask_shape)
965
+ return out_arr
966
+
967
+
968
+ def make_structure(
969
+ flat_layout: AtomLayout,
970
+ atom_coords: np.ndarray,
971
+ name: str,
972
+ *,
973
+ atom_b_factors: np.ndarray | None = None,
974
+ all_physical_residues: Residues | None = None,
975
+ ) -> structure.Structure:
976
+ """Returns a Structure from a flat layout and atom coordinates.
977
+
978
+ The provided flat_layout must be 1-dim and must not contain any padding
979
+ elements. The flat_layout.atom_name must conform to the OpenMM/CCD standard
980
+ and must not contain deuterium.
981
+
982
+ Args:
983
+ flat_layout: flat 1-dim AtomLayout without pading elements
984
+ atom_coords: np.ndarray of float, shape (num_atoms, 3)
985
+ name: str: the name (usually PDB id), e.g. '1uao'
986
+ atom_b_factors: np.ndarray of float, shape (num_atoms,) or None. If None,
987
+ they will be set to all zeros.
988
+ all_physical_residues: a Residues object that contains all physically
989
+ existing residues, i.e. also those residues that have no resolved atoms.
990
+ This is common in experimental structures, but also appears in predicted
991
+ structures for 'UNK' or other non-standard residue types, where the model
992
+ does not predict coordinates. This will be used to create the
993
+ `all_residues` field of the structure object.
994
+ """
995
+
996
+ if flat_layout.atom_name.ndim != 1 or not np.all(
997
+ flat_layout.atom_name.astype(bool)
998
+ ):
999
+ raise ValueError(
1000
+ 'flat_layout must be 1-dim and must not contain anypadding element'
1001
+ )
1002
+ if (
1003
+ flat_layout.atom_element is None
1004
+ or flat_layout.res_name is None
1005
+ or flat_layout.chain_type is None
1006
+ ):
1007
+ raise ValueError('All optional fields must be present.')
1008
+
1009
+ if atom_b_factors is None:
1010
+ atom_b_factors = np.zeros(atom_coords.shape[:-1])
1011
+
1012
+ if all_physical_residues is not None:
1013
+ # Create the all_residues field from a Residues object
1014
+ # (unfortunately there is no central place to keep the chain_types in
1015
+ # the structure class, so we drop it here)
1016
+ all_residues = collections.defaultdict(list)
1017
+ for chain_id, res_id, res_name in zip(
1018
+ all_physical_residues.chain_id,
1019
+ all_physical_residues.res_id,
1020
+ all_physical_residues.res_name,
1021
+ strict=True,
1022
+ ):
1023
+ all_residues[chain_id].append((res_name, res_id))
1024
+ else:
1025
+ # Create the all_residues field from the flat_layout
1026
+ all_residues = collections.defaultdict(list)
1027
+ if flat_layout.chain_id.shape[0] > 0:
1028
+ all_residues[flat_layout.chain_id[0]].append(
1029
+ (flat_layout.res_name[0], flat_layout.res_id[0])
1030
+ )
1031
+ for i in range(1, flat_layout.shape[0]):
1032
+ if (
1033
+ flat_layout.chain_id[i] != flat_layout.chain_id[i - 1]
1034
+ or flat_layout.res_name[i] != flat_layout.res_name[i - 1]
1035
+ or flat_layout.res_id[i] != flat_layout.res_id[i - 1]
1036
+ ):
1037
+ all_residues[flat_layout.chain_id[i]].append(
1038
+ (flat_layout.res_name[i], flat_layout.res_id[i])
1039
+ )
1040
+
1041
+ return structure.from_atom_arrays(
1042
+ name=name,
1043
+ all_residues=dict(all_residues),
1044
+ chain_id=flat_layout.chain_id,
1045
+ chain_type=flat_layout.chain_type,
1046
+ res_id=flat_layout.res_id.astype(np.int32),
1047
+ res_name=flat_layout.res_name,
1048
+ atom_name=flat_layout.atom_name,
1049
+ atom_element=flat_layout.atom_element,
1050
+ atom_x=atom_coords[..., 0],
1051
+ atom_y=atom_coords[..., 1],
1052
+ atom_z=atom_coords[..., 2],
1053
+ atom_b_factor=atom_b_factors,
1054
+ )
1055
+
1056
+
1057
+ def _assert_all_arrays_have_same_shape(
1058
+ *,
1059
+ obj: AtomLayout | Residues | GatherInfo,
1060
+ expected_shape: tuple[int, ...],
1061
+ attribute_names: Sequence[str],
1062
+ ) -> None:
1063
+ """Checks that given attributes of the object have the expected shape."""
1064
+ attribute_shapes_description = []
1065
+ all_shapes_are_valid = True
1066
+
1067
+ for attribute_name in attribute_names:
1068
+ attribute = getattr(obj, attribute_name)
1069
+
1070
+ if attribute is None:
1071
+ attribute_shape = None
1072
+ else:
1073
+ attribute_shape = attribute.shape
1074
+
1075
+ if attribute_shape is not None and expected_shape != attribute_shape:
1076
+ all_shapes_are_valid = False
1077
+
1078
+ attribute_shape_name = attribute_name + '.shape'
1079
+ attribute_shapes_description.append(
1080
+ f'{attribute_shape_name:25} = {attribute_shape}'
1081
+ )
1082
+
1083
+ if not all_shapes_are_valid:
1084
+ raise ValueError(
1085
+ f'All arrays must have the same shape ({expected_shape=}). Got\n'
1086
+ + '\n'.join(attribute_shapes_description)
1087
+ )
flax_model/alphafold3/model/components/haiku_modules.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Common Haiku modules."""
4
+
5
+ from collections.abc import Sequence
6
+ import contextlib
7
+ import numbers
8
+ from typing import TypeAlias
9
+
10
+ import haiku as hk
11
+ import jax
12
+ import jax.numpy as jnp
13
+ import numpy as np
14
+
15
+
16
+ PRECISION: TypeAlias = (
17
+ None
18
+ | str
19
+ | jax.lax.Precision
20
+ | tuple[str, str]
21
+ | tuple[jax.lax.Precision, jax.lax.Precision]
22
+ )
23
+
24
+ # Useful for mocking in tests.
25
+ DEFAULT_PRECISION = None
26
+
27
+ # Constant from scipy.stats.truncnorm.std(a=-2, b=2, loc=0., scale=1.)
28
+ TRUNCATED_NORMAL_STDDEV_FACTOR = np.asarray(
29
+ 0.87962566103423978, dtype=np.float32
30
+ )
31
+
32
+
33
+ class LayerNorm(hk.LayerNorm):
34
+ """LayerNorm module.
35
+
36
+ Equivalent to hk.LayerNorm but with an extra 'upcast' option that casts
37
+ (b)float16 inputs to float32 before computing the layer norm, and then casts
38
+ the output back to the input type.
39
+
40
+ The learnable parameter shapes are also different from Haiku: they are always
41
+ vectors rather than possibly higher-rank tensors. This makes it easier
42
+ to change the layout whilst keep the model weight-compatible.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ *,
48
+ axis: int = -1,
49
+ create_scale: bool = True,
50
+ create_offset: bool = True,
51
+ eps: float = 1e-5,
52
+ scale_init: hk.initializers.Initializer | None = None,
53
+ offset_init: hk.initializers.Initializer | None = None,
54
+ use_fast_variance: bool = True,
55
+ name: str,
56
+ param_axis: int | None = None,
57
+ upcast: bool = True,
58
+ ):
59
+ super().__init__(
60
+ axis=axis,
61
+ create_scale=False,
62
+ create_offset=False,
63
+ eps=eps,
64
+ scale_init=None,
65
+ offset_init=None,
66
+ use_fast_variance=use_fast_variance,
67
+ name=name,
68
+ param_axis=param_axis,
69
+ )
70
+ self.upcast = upcast
71
+ self._temp_create_scale = create_scale
72
+ self._temp_create_offset = create_offset
73
+
74
+ def __call__(self, x: jnp.ndarray) -> jnp.ndarray:
75
+ dtype = x.dtype
76
+ is_16bit = x.dtype in [jnp.bfloat16, jnp.float16]
77
+ if self.upcast and is_16bit:
78
+ x = x.astype(jnp.float32)
79
+
80
+ param_axis = self.param_axis[0] if self.param_axis else -1
81
+ param_shape = (x.shape[param_axis],)
82
+
83
+ param_broadcast_shape = [1] * x.ndim
84
+ param_broadcast_shape[param_axis] = x.shape[param_axis]
85
+ scale = None
86
+ offset = None
87
+ if self._temp_create_scale:
88
+ scale = hk.get_parameter(
89
+ 'scale', param_shape, x.dtype, init=self.scale_init
90
+ )
91
+ scale = scale.reshape(param_broadcast_shape)
92
+
93
+ if self._temp_create_offset:
94
+ offset = hk.get_parameter(
95
+ 'offset', param_shape, x.dtype, init=self.offset_init
96
+ )
97
+ offset = offset.reshape(param_broadcast_shape)
98
+
99
+ out = super().__call__(x, scale=scale, offset=offset)
100
+
101
+ if self.upcast and is_16bit:
102
+ out = out.astype(dtype)
103
+
104
+ return out
105
+
106
+
107
+ def haiku_linear_get_params(
108
+ inputs: jax.Array | jax.ShapeDtypeStruct,
109
+ *,
110
+ num_output: int | Sequence[int],
111
+ use_bias: bool = False,
112
+ num_input_dims: int = 1,
113
+ initializer: str = 'linear',
114
+ bias_init: float = 0.0,
115
+ transpose_weights: bool = False,
116
+ name: str | None = None,
117
+ ) -> tuple[jax.Array, jax.Array | None]:
118
+ """Get parameters for linear layer.
119
+
120
+ Parameters will be at least float32 or higher precision.
121
+
122
+ Arguments:
123
+ inputs: The input to the Linear layer. Can be either a JAX array or a
124
+ jax.ShapeDtypeStruct.
125
+ num_output: The number of output channels. Can be an integer or a sequence
126
+ of integers.
127
+ use_bias: Whether to create a bias array.
128
+ num_input_dims: The number of dimensions to consider as channel dims in the
129
+ input.
130
+ initializer: The name of the weight initializer to use.
131
+ bias_init: A float used to initialize the bias.
132
+ transpose_weights: If True, will create a transposed version of the weights.
133
+ name: The Haiku namespace to use for the weight and bias.
134
+
135
+ Returns:
136
+ A tuple[weight, bias] if use_bias otherwise tuple[weight, None].
137
+ """
138
+
139
+ if isinstance(num_output, numbers.Integral):
140
+ output_shape = (num_output,)
141
+ else:
142
+ output_shape = tuple(num_output)
143
+
144
+ if num_input_dims > 0:
145
+ in_shape = inputs.shape[-num_input_dims:]
146
+ elif num_input_dims == 0:
147
+ in_shape = ()
148
+ else:
149
+ raise ValueError('num_input_dims must be >= 0.')
150
+
151
+ weight_init = _get_initializer_scale(initializer, in_shape)
152
+ with hk.name_scope(name) if name else contextlib.nullcontext():
153
+
154
+ if transpose_weights:
155
+ weight_shape = output_shape + in_shape
156
+
157
+ weights = hk.get_parameter(
158
+ 'weights', shape=weight_shape, dtype=inputs.dtype, init=weight_init
159
+ )
160
+ else:
161
+ weight_shape = in_shape + output_shape
162
+ weights = hk.get_parameter(
163
+ name='weights',
164
+ shape=weight_shape,
165
+ dtype=inputs.dtype,
166
+ init=weight_init,
167
+ )
168
+
169
+ bias = None
170
+ if use_bias:
171
+ bias = hk.get_parameter(
172
+ name='bias',
173
+ shape=output_shape,
174
+ dtype=inputs.dtype,
175
+ init=hk.initializers.Constant(bias_init),
176
+ )
177
+ return weights, bias
178
+
179
+
180
+ class Linear(hk.Module):
181
+ """Custom Linear Module.
182
+
183
+ This differs from the standard Linear in a few ways:
184
+ * It supports inputs of arbitrary rank
185
+ * It allows to use ntk parametrization
186
+ * Initializers are specified by strings
187
+ * It allows to explicitly specify which dimension of the input will map to
188
+ the tpu sublane/lane dimensions.
189
+ """
190
+
191
+ def __init__(
192
+ self,
193
+ num_output: int | Sequence[int],
194
+ *,
195
+ initializer: str = 'linear',
196
+ num_input_dims: int = 1,
197
+ use_bias: bool = False,
198
+ bias_init: float = 0.0,
199
+ precision: PRECISION = None,
200
+ fast_scalar_mode: bool = True,
201
+ transpose_weights: bool = False,
202
+ name: str,
203
+ ):
204
+ """Constructs Linear Module.
205
+
206
+ Args:
207
+ num_output: number of output channels. Can be tuple when outputting
208
+ multiple dimensions.
209
+ initializer: What initializer to use, should be one of {'linear', 'relu',
210
+ 'zeros'}.
211
+ num_input_dims: Number of dimensions from the end to project.
212
+ use_bias: Whether to include trainable bias (False by default).
213
+ bias_init: Value used to initialize bias.
214
+ precision: What precision to use for matrix multiplication, defaults to
215
+ None.
216
+ fast_scalar_mode: Whether to use optimized path for num_input_dims = 0.
217
+ transpose_weights: decides whether weights have shape [input, output] or
218
+ [output, input], True means [output, input], this is helpful to avoid
219
+ padding on the tensors holding the weights.
220
+ name: name of module, used for name scopes.
221
+ """
222
+ super().__init__(name=name)
223
+ if isinstance(num_output, numbers.Integral):
224
+ self.output_shape = (num_output,)
225
+ else:
226
+ self.output_shape = tuple(num_output)
227
+ self.initializer = initializer
228
+ self.use_bias = use_bias
229
+ self.bias_init = bias_init
230
+ self.num_input_dims = num_input_dims
231
+ self.num_output_dims = len(self.output_shape)
232
+ self.precision = precision if precision is not None else DEFAULT_PRECISION
233
+ self.fast_scalar_mode = fast_scalar_mode
234
+ self.transpose_weights = transpose_weights
235
+
236
+ def __call__(self, inputs: jnp.ndarray) -> jnp.ndarray:
237
+ """Connects Module.
238
+
239
+ Args:
240
+ inputs: Tensor of shape [..., num_channel]
241
+
242
+ Returns:
243
+ output of shape [..., num_output]
244
+ """
245
+
246
+ num_input_dims = self.num_input_dims
247
+
248
+ # Adds specialized path for scalar inputs in Linear layer,
249
+ # this means the linear Layer does not use the matmul units on the tpu,
250
+ # which is more efficient and gives compiler more flexibility over layout.
251
+ if num_input_dims == 0 and self.fast_scalar_mode:
252
+ weight_shape = self.output_shape
253
+ if self.initializer == 'zeros':
254
+ w_init = hk.initializers.Constant(0.0)
255
+ else:
256
+ distribution_stddev = jnp.array(1 / TRUNCATED_NORMAL_STDDEV_FACTOR)
257
+ w_init = hk.initializers.TruncatedNormal(
258
+ mean=0.0, stddev=distribution_stddev
259
+ )
260
+
261
+ weights = hk.get_parameter('weights', weight_shape, inputs.dtype, w_init)
262
+
263
+ inputs = jnp.expand_dims(
264
+ inputs, tuple(range(-1, -self.num_output_dims - 1, -1))
265
+ )
266
+ output = inputs * weights
267
+ else:
268
+ if self.num_input_dims > 0:
269
+ in_shape = inputs.shape[-self.num_input_dims :]
270
+ else:
271
+ in_shape = ()
272
+
273
+ weight_init = _get_initializer_scale(self.initializer, in_shape)
274
+
275
+ in_letters = 'abcde'[: self.num_input_dims]
276
+ out_letters = 'hijkl'[: self.num_output_dims]
277
+
278
+ if self.transpose_weights:
279
+ weight_shape = self.output_shape + in_shape
280
+ weights = hk.get_parameter(
281
+ 'weights', weight_shape, inputs.dtype, weight_init
282
+ )
283
+ equation = (
284
+ f'...{in_letters}, {out_letters}{in_letters}->...{out_letters}'
285
+ )
286
+ else:
287
+ weight_shape = in_shape + self.output_shape
288
+ weights = hk.get_parameter(
289
+ 'weights', weight_shape, inputs.dtype, weight_init
290
+ )
291
+
292
+ equation = (
293
+ f'...{in_letters}, {in_letters}{out_letters}->...{out_letters}'
294
+ )
295
+
296
+ output = jnp.einsum(equation, inputs, weights, precision=self.precision)
297
+
298
+ if self.use_bias:
299
+ bias = hk.get_parameter(
300
+ 'bias',
301
+ self.output_shape,
302
+ inputs.dtype,
303
+ hk.initializers.Constant(self.bias_init),
304
+ )
305
+ output += bias
306
+
307
+ return output
308
+
309
+
310
+ def _get_initializer_scale(initializer_name, input_shape):
311
+ """Get initializer for weights."""
312
+
313
+ if initializer_name == 'zeros':
314
+ w_init = hk.initializers.Constant(0.0)
315
+ else:
316
+ # fan-in scaling
317
+ noise_scale = 1.0
318
+ for channel_dim in input_shape:
319
+ noise_scale /= channel_dim
320
+ if initializer_name == 'relu':
321
+ noise_scale *= 2
322
+
323
+ stddev = np.sqrt(noise_scale)
324
+ # Adjust stddev for truncation.
325
+ stddev = stddev / TRUNCATED_NORMAL_STDDEV_FACTOR
326
+ w_init = hk.initializers.TruncatedNormal(mean=0.0, stddev=stddev)
327
+
328
+ return w_init
flax_model/alphafold3/model/components/mapping.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Specialized mapping functions."""
4
+
5
+ from collections.abc import Callable, Sequence
6
+ import functools
7
+ from typing import Any, TypeVar
8
+
9
+ import haiku as hk
10
+ import jax
11
+ import jax.numpy as jnp
12
+
13
+
14
+ Pytree = Any
15
+ PytreeJaxArray = Any
16
+
17
+ partial = functools.partial
18
+ PROXY = object()
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ def _maybe_slice(array, i, slice_size, axis):
24
+ if axis is PROXY:
25
+ return array
26
+ else:
27
+ return jax.lax.dynamic_slice_in_dim(
28
+ array, i, slice_size=slice_size, axis=axis
29
+ )
30
+
31
+
32
+ def _maybe_get_size(array, axis):
33
+ if axis == PROXY:
34
+ return -1
35
+ else:
36
+ return array.shape[axis]
37
+
38
+
39
+ def _expand_axes(axes, values, name="sharded_apply"):
40
+ values_tree_def = jax.tree_util.tree_structure(values)
41
+ flat_axes = jax.api_util.flatten_axes(name, values_tree_def, axes)
42
+ # Replace None's with PROXY.
43
+ flat_axes = [PROXY if x is None else x for x in flat_axes]
44
+ return jax.tree_util.tree_unflatten(values_tree_def, flat_axes)
45
+
46
+
47
+ def sharded_map(
48
+ fun: Callable[..., PytreeJaxArray],
49
+ shard_size: int | None = 1,
50
+ in_axes: int | Pytree = 0,
51
+ out_axes: int | Pytree = 0,
52
+ ) -> Callable[..., PytreeJaxArray]:
53
+ """Sharded vmap.
54
+
55
+ Maps `fun` over axes, in a way similar to hk.vmap, but does so in shards of
56
+ `shard_size`. This allows a smooth trade-off between memory usage
57
+ (as in a plain map) vs higher throughput (as in a vmap).
58
+
59
+ Args:
60
+ fun: Function to apply smap transform to.
61
+ shard_size: Integer denoting shard size.
62
+ in_axes: Either integer or pytree describing which axis to map over for each
63
+ input to `fun`, None denotes broadcasting.
64
+ out_axes: Integer or pytree denoting to what axis in the output the mapped
65
+ over axis maps.
66
+
67
+ Returns:
68
+ Function with smap applied.
69
+ """
70
+ if hk.running_init():
71
+ # Guarantees initialisation independent of shard_size. Doesn't incur a high
72
+ # memory cost, as long as large concrete tensors are not encountered.
73
+ return hk.vmap(fun, in_axes=in_axes, out_axes=out_axes, split_rng=False)
74
+ else:
75
+ vmapped_fun = hk.vmap(fun, in_axes, out_axes, split_rng=True)
76
+ return sharded_apply(vmapped_fun, shard_size, in_axes, out_axes)
77
+
78
+
79
+ def _set_docstring(docstr: str) -> Callable[[T], T]:
80
+ """Decorator for setting the docstring of a function."""
81
+
82
+ def wrapped(fun: T) -> T:
83
+ fun.__doc__ = docstr.format(fun=getattr(fun, "__name__", repr(fun)))
84
+ return fun
85
+
86
+ return wrapped
87
+
88
+
89
+ def sharded_apply(
90
+ fun: Callable[..., PytreeJaxArray],
91
+ shard_size: int | None = 1,
92
+ in_axes: int | Pytree = 0,
93
+ out_axes: int | Pytree = 0,
94
+ new_out_axes: bool = False,
95
+ ) -> Callable[..., PytreeJaxArray]:
96
+ """Sharded apply.
97
+
98
+ Applies `fun` over shards to axes, in a way similar to vmap,
99
+ but does so in shards of `shard_size`. Shards are stacked after.
100
+ This allows a smooth trade-off between
101
+ memory usage (as in a plain map) vs higher throughput (as in a vmap).
102
+
103
+ Args:
104
+ fun: Function to apply smap transform to.
105
+ shard_size: Integer denoting shard size. None will return `fun` unchanged.
106
+ in_axes: Either integer or pytree describing which axis to map over for each
107
+ input to `fun`, None denotes broadcasting.
108
+ out_axes: Integer or pytree denoting to what axis in the output the mapped
109
+ over axis maps.
110
+ new_out_axes: Whether to stack outputs on new axes. This assumes that the
111
+ output sizes for each shard (including the possible remainder shard) are
112
+ the same.
113
+
114
+ Returns:
115
+ Function with smap applied.
116
+ """
117
+ docstr = (
118
+ "Mapped version of {fun}. Takes similar arguments to {fun} "
119
+ "but with additional array axes over which {fun} is mapped."
120
+ )
121
+ if new_out_axes:
122
+ raise NotImplementedError("New output axes not yet implemented.")
123
+
124
+ if shard_size is None:
125
+ return fun
126
+
127
+ @_set_docstring(docstr)
128
+ @functools.wraps(fun)
129
+ def mapped_fn(*args, **kwargs):
130
+ # Expand in axes and determine loop range.
131
+ in_axes_ = _expand_axes(in_axes, args)
132
+
133
+ in_sizes = jax.tree.map(_maybe_get_size, args, in_axes_)
134
+ in_size = max(jax.tree_util.tree_leaves(in_sizes))
135
+
136
+ num_extra_shards = (in_size - 1) // shard_size
137
+
138
+ # Fix if necessary.
139
+ last_shard_size = in_size % shard_size
140
+ last_shard_size = shard_size if last_shard_size == 0 else last_shard_size
141
+
142
+ def apply_fun_to_slice(slice_start, slice_size):
143
+ input_slice = jax.tree.map(
144
+ lambda array, axis: _maybe_slice(
145
+ array, slice_start, slice_size, axis
146
+ ),
147
+ args,
148
+ in_axes_,
149
+ )
150
+ return fun(*input_slice, **kwargs)
151
+
152
+ remainder_shape_dtype = hk.eval_shape(
153
+ partial(apply_fun_to_slice, 0, last_shard_size)
154
+ )
155
+ out_dtypes = jax.tree.map(lambda x: x.dtype, remainder_shape_dtype)
156
+ out_shapes = jax.tree.map(lambda x: x.shape, remainder_shape_dtype)
157
+ out_axes_ = _expand_axes(out_axes, remainder_shape_dtype)
158
+
159
+ if num_extra_shards > 0:
160
+ regular_shard_shape_dtype = hk.eval_shape(
161
+ partial(apply_fun_to_slice, 0, shard_size)
162
+ )
163
+ shard_shapes = jax.tree.map(lambda x: x.shape, regular_shard_shape_dtype)
164
+
165
+ def make_output_shape(axis, shard_shape, remainder_shape):
166
+ return (
167
+ shard_shape[:axis]
168
+ + (shard_shape[axis] * num_extra_shards + remainder_shape[axis],)
169
+ + shard_shape[axis + 1 :]
170
+ )
171
+
172
+ out_shapes = jax.tree.map(
173
+ make_output_shape, out_axes_, shard_shapes, out_shapes
174
+ )
175
+
176
+ # Calls dynamic Update slice with different argument order.
177
+ # This is here since tree_map only works with positional arguments.
178
+ def dynamic_update_slice_in_dim(full_array, update, axis, i):
179
+ return jax.lax.dynamic_update_slice_in_dim(full_array, update, i, axis)
180
+
181
+ def compute_shard(outputs, slice_start, slice_size):
182
+ slice_out = apply_fun_to_slice(slice_start, slice_size)
183
+ update_slice = partial(dynamic_update_slice_in_dim, i=slice_start)
184
+ return jax.tree.map(update_slice, outputs, slice_out, out_axes_)
185
+
186
+ def scan_iteration(outputs, i):
187
+ new_outputs = compute_shard(outputs, i, shard_size)
188
+ return new_outputs, ()
189
+
190
+ slice_starts = jnp.arange(0, in_size - shard_size + 1, shard_size)
191
+
192
+ def allocate_buffer(dtype, shape):
193
+ return jnp.zeros(shape, dtype=dtype)
194
+
195
+ outputs = jax.tree.map(allocate_buffer, out_dtypes, out_shapes)
196
+
197
+ if slice_starts.shape[0] > 0:
198
+ outputs, _ = hk.scan(scan_iteration, outputs, slice_starts)
199
+
200
+ if last_shard_size != shard_size:
201
+ remainder_start = in_size - last_shard_size
202
+ outputs = compute_shard(outputs, remainder_start, last_shard_size)
203
+
204
+ return outputs
205
+
206
+ return mapped_fn
207
+
208
+
209
+ def inference_subbatch(
210
+ module: Callable[..., PytreeJaxArray],
211
+ subbatch_size: int,
212
+ batched_args: Sequence[PytreeJaxArray],
213
+ nonbatched_args: Sequence[PytreeJaxArray],
214
+ input_subbatch_dim: int = 0,
215
+ output_subbatch_dim: int | None = None,
216
+ ) -> PytreeJaxArray:
217
+ """Run through subbatches (like batch apply but with split and concat)."""
218
+ assert len(batched_args) > 0 # pylint: disable=g-explicit-length-test
219
+
220
+ if hk.running_init():
221
+ args = list(batched_args) + list(nonbatched_args)
222
+ return module(*args)
223
+
224
+ if output_subbatch_dim is None:
225
+ output_subbatch_dim = input_subbatch_dim
226
+
227
+ def run_module(*batched_args):
228
+ args = list(batched_args) + list(nonbatched_args)
229
+ res = module(*args)
230
+ return res
231
+
232
+ sharded_module = sharded_apply(
233
+ run_module,
234
+ shard_size=subbatch_size,
235
+ in_axes=input_subbatch_dim,
236
+ out_axes=output_subbatch_dim,
237
+ )
238
+ output = sharded_module(*batched_args)
239
+
240
+ return output
flax_model/alphafold3/model/components/utils.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Utility functions for training AlphaFold and similar models."""
4
+
5
+ from collections import abc
6
+ import contextlib
7
+ import numbers
8
+
9
+ from flax_model.alphafold3.model import features
10
+ import haiku as hk
11
+ import jax.numpy as jnp
12
+ import numpy as np
13
+
14
+
15
+ VALID_DTYPES = [np.float32, np.float64, np.int8, np.int32, np.int64, bool]
16
+
17
+
18
+ def remove_invalidly_typed_feats(
19
+ batch: features.BatchDict,
20
+ ) -> features.BatchDict:
21
+ """Remove features of types we don't want to send to the TPU e.g. strings."""
22
+ return {
23
+ k: v
24
+ for k, v in batch.items()
25
+ if hasattr(v, 'dtype') and v.dtype in VALID_DTYPES
26
+ }
27
+
28
+
29
+ def bfloat16_getter(next_getter, value, context):
30
+ """Ensures that a bfloat16 parameter is provided by casting if necessary."""
31
+ if context.original_dtype == jnp.bfloat16:
32
+ if value.dtype != jnp.bfloat16:
33
+ value = value.astype(jnp.bfloat16)
34
+ return next_getter(value)
35
+
36
+
37
+ @contextlib.contextmanager
38
+ def bfloat16_context():
39
+ with hk.custom_getter(bfloat16_getter):
40
+ yield
41
+
42
+
43
+ def mask_mean(mask, value, axis=None, keepdims=False, eps=1e-10):
44
+ """Masked mean."""
45
+
46
+ mask_shape = mask.shape
47
+ value_shape = value.shape
48
+
49
+ assert len(mask_shape) == len(
50
+ value_shape
51
+ ), 'Shapes are not compatible, shapes: {}, {}'.format(mask_shape, value_shape)
52
+
53
+ if isinstance(axis, numbers.Integral):
54
+ axis = [axis]
55
+ elif axis is None:
56
+ axis = list(range(len(mask_shape)))
57
+ assert isinstance(
58
+ axis, abc.Iterable
59
+ ), 'axis needs to be either an iterable, integer or "None"'
60
+
61
+ broadcast_factor = 1.0
62
+ for axis_ in axis:
63
+ value_size = value_shape[axis_]
64
+ mask_size = mask_shape[axis_]
65
+ if mask_size == 1:
66
+ broadcast_factor *= value_size
67
+ else:
68
+ error = f'Shapes are not compatible, shapes: {mask_shape}, {value_shape}'
69
+ assert mask_size == value_size, error
70
+
71
+ return jnp.sum(mask * value, keepdims=keepdims, axis=axis) / (
72
+ jnp.maximum(
73
+ jnp.sum(mask, keepdims=keepdims, axis=axis) * broadcast_factor, eps
74
+ )
75
+ )
flax_model/alphafold3/model/confidence_types.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Confidence categories for predictions."""
4
+
5
+ import dataclasses
6
+ import enum
7
+ import json
8
+ from typing import Any, Self
9
+
10
+ from absl import logging
11
+ from flax_model.alphafold3.model import model
12
+ import jax
13
+ import numpy as np
14
+
15
+
16
+ class StructureConfidenceFullEncoder(json.JSONEncoder):
17
+ """JSON encoder for serializing confidence types."""
18
+
19
+ def __init__(self, **kwargs):
20
+ super().__init__(**(kwargs | dict(separators=(',', ':'))))
21
+
22
+ def encode(self, o: 'StructureConfidenceFull'):
23
+ # Cast to np.float64 before rounding, since casting to Python float will
24
+ # cast to a 64 bit float, potentially undoing np.float32 rounding.
25
+ atom_plddts = np.round(
26
+ np.clip(np.asarray(o.atom_plddts, dtype=np.float64), 0.0, 99.99), 2
27
+ ).astype(float)
28
+ contact_probs = np.round(
29
+ np.clip(np.asarray(o.contact_probs, dtype=np.float64), 0.0, 1.0), 2
30
+ ).astype(float)
31
+ pae = np.round(
32
+ np.clip(np.asarray(o.pae, dtype=np.float64), 0.0, 99.9), 1
33
+ ).astype(float)
34
+ return """\
35
+ {
36
+ "atom_chain_ids": %s,
37
+ "atom_plddts": %s,
38
+ "contact_probs": %s,
39
+ "pae": %s,
40
+ "token_chain_ids": %s,
41
+ "token_res_ids": %s
42
+ }""" % (
43
+ super().encode(o.atom_chain_ids),
44
+ super().encode(list(atom_plddts)).replace('NaN', 'null'),
45
+ super().encode([list(x) for x in contact_probs]).replace('NaN', 'null'),
46
+ super().encode([list(x) for x in pae]).replace('NaN', 'null'),
47
+ super().encode(o.token_chain_ids),
48
+ super().encode(o.token_res_ids),
49
+ )
50
+
51
+
52
+ def _dump_json(data: Any, indent: int | None = None) -> str:
53
+ """Dumps a json string with JSON compatible NaN representation."""
54
+ json_str = json.dumps(
55
+ data,
56
+ sort_keys=True,
57
+ indent=indent,
58
+ separators=(',', ': '),
59
+ )
60
+ return json_str.replace('NaN', 'null')
61
+
62
+
63
+ @enum.unique
64
+ class ConfidenceCategory(enum.Enum):
65
+ """Confidence categories for AlphaFold predictions."""
66
+
67
+ HIGH = 0
68
+ MEDIUM = 1
69
+ LOW = 2
70
+ DISORDERED = 3
71
+
72
+ @classmethod
73
+ def from_char(cls, char: str) -> Self:
74
+ match char:
75
+ case 'H':
76
+ return cls.HIGH
77
+ case 'M':
78
+ return cls.MEDIUM
79
+ case 'L':
80
+ return cls.LOW
81
+ case 'D':
82
+ return cls.DISORDERED
83
+ case _:
84
+ raise ValueError(
85
+ f'Unknown character. Expected one of H, M, L or D; got: {char}'
86
+ )
87
+
88
+ def to_char(self) -> str:
89
+ match self:
90
+ case self.HIGH:
91
+ return 'H'
92
+ case self.MEDIUM:
93
+ return 'M'
94
+ case self.LOW:
95
+ return 'L'
96
+ case self.DISORDERED:
97
+ return 'D'
98
+
99
+ @classmethod
100
+ def from_confidence_score(cls, confidence: float) -> Self:
101
+ if 90 <= confidence <= 100:
102
+ return cls.HIGH
103
+ if 70 <= confidence < 90:
104
+ return cls.MEDIUM
105
+ if 50 <= confidence < 70:
106
+ return cls.LOW
107
+ if 0 <= confidence < 50:
108
+ return cls.DISORDERED
109
+ raise ValueError(f'Confidence score out of range [0, 100]: {confidence}')
110
+
111
+
112
+ @dataclasses.dataclass()
113
+ class AtomConfidence:
114
+ """Dataclass for 1D per-atom confidences from AlphaFold."""
115
+
116
+ chain_id: list[str]
117
+ atom_number: list[int]
118
+ confidence: list[float]
119
+ confidence_category: list[ConfidenceCategory]
120
+
121
+ def __post_init__(self):
122
+ num_res = len(self.atom_number)
123
+ if not all(
124
+ len(v) == num_res
125
+ for v in [self.chain_id, self.confidence, self.confidence_category]
126
+ ):
127
+ raise ValueError('All confidence fields must have the same length.')
128
+
129
+ @classmethod
130
+ def from_inference_result(
131
+ cls, inference_result: model.InferenceResult
132
+ ) -> Self:
133
+ """Instantiates an AtomConfidence from a structure.
134
+
135
+ Args:
136
+ inference_result: Inference result from AlphaFold.
137
+
138
+ Returns:
139
+ Scores in AtomConfidence dataclass.
140
+ """
141
+ struc = inference_result.predicted_structure
142
+ as_dict = {
143
+ 'chain_id': [],
144
+ 'atom_number': [],
145
+ 'confidence': [],
146
+ 'confidence_category': [],
147
+ }
148
+ for atom_number, atom in enumerate(struc.iter_atoms()):
149
+ this_confidence = float(struc.atom_b_factor[atom_number])
150
+ as_dict['chain_id'].append(atom['chain_id'])
151
+ as_dict['atom_number'].append(atom_number)
152
+ as_dict['confidence'].append(round(this_confidence, 2))
153
+ as_dict['confidence_category'].append(
154
+ ConfidenceCategory.from_confidence_score(this_confidence)
155
+ )
156
+ return cls(**as_dict)
157
+
158
+ @classmethod
159
+ def from_json(cls, json_string: str) -> Self:
160
+ """Instantiates a AtomConfidence from a json string."""
161
+ input_dict = json.loads(json_string)
162
+ input_dict['confidence_category'] = [
163
+ ConfidenceCategory.from_char(k)
164
+ for k in input_dict['confidence_category']
165
+ ]
166
+ return cls(**input_dict)
167
+
168
+ def to_json(self) -> str:
169
+ output = dataclasses.asdict(self)
170
+ output['confidence_category'] = [
171
+ k.to_char() for k in output['confidence_category']
172
+ ]
173
+ output['atom_number'] = [int(k) for k in output['atom_number']]
174
+ return _dump_json(output)
175
+
176
+
177
+ @dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
178
+ class StructureConfidenceSummary:
179
+ """Dataclass for the summary of structure scores from AlphaFold.
180
+
181
+ Attributes:
182
+ ptm: Predicted TM global score.
183
+ iptm: Interface predicted TM global score.
184
+ ranking_score: Ranking score extracted from CIF metadata.
185
+ fraction_disordered: Fraction disordered, measured with RASA.
186
+ has_clash: Has significant clashing.
187
+ chain_pair_pae_min: [num_chains, num_chains] Minimum cross chain PAE.
188
+ chain_pair_iptm: [num_chains, num_chains] Chain pair ipTM.
189
+ chain_ptm: [num_chains] Chain pTM.
190
+ chain_iptm: [num_chains] Mean cross chain ipTM for a chain.
191
+ """
192
+
193
+ ptm: float
194
+ iptm: float
195
+ ranking_score: float
196
+ fraction_disordered: float
197
+ has_clash: float
198
+ chain_pair_pae_min: np.ndarray
199
+ chain_pair_iptm: np.ndarray
200
+ chain_ptm: np.ndarray
201
+ chain_iptm: np.ndarray
202
+
203
+ @classmethod
204
+ def from_inference_result(
205
+ cls, inference_result: model.InferenceResult
206
+ ) -> Self:
207
+ """Returns a new instance based on a given inference result."""
208
+ return cls(
209
+ ptm=float(inference_result.metadata['ptm']),
210
+ iptm=float(inference_result.metadata['iptm']),
211
+ ranking_score=float(inference_result.metadata['ranking_score']),
212
+ fraction_disordered=float(
213
+ inference_result.metadata['fraction_disordered']
214
+ ),
215
+ has_clash=float(inference_result.metadata['has_clash']),
216
+ chain_pair_pae_min=inference_result.metadata['chain_pair_pae_min'],
217
+ chain_pair_iptm=inference_result.metadata['chain_pair_iptm'],
218
+ chain_ptm=inference_result.metadata['iptm_ichain'],
219
+ chain_iptm=inference_result.metadata['iptm_xchain'],
220
+ )
221
+
222
+ @classmethod
223
+ def from_json(cls, json_string: str) -> Self:
224
+ """Returns a new instance from a given json string."""
225
+ return cls(**json.loads(json_string))
226
+
227
+ def to_json(self) -> str:
228
+ def convert(data):
229
+ if isinstance(data, np.ndarray):
230
+ # Cast to np.float64 before rounding, since casting to Python float will
231
+ # cast to a 64 bit float, potentially undoing np.float32 rounding.
232
+ rounded_data = np.round(data.astype(np.float64), decimals=2).tolist()
233
+ else:
234
+ rounded_data = np.round(data, decimals=2)
235
+ return rounded_data
236
+
237
+ return _dump_json(jax.tree.map(convert, dataclasses.asdict(self)), indent=1)
238
+
239
+
240
+ @dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
241
+ class StructureConfidenceFull:
242
+ """Dataclass for full structure data from AlphaFold."""
243
+
244
+ pae: np.ndarray
245
+ token_chain_ids: list[str]
246
+ token_res_ids: list[int]
247
+ atom_plddts: list[float]
248
+ atom_chain_ids: list[str]
249
+ contact_probs: np.ndarray # [num_tokens, num_tokens]
250
+
251
+ @classmethod
252
+ def from_inference_result(
253
+ cls, inference_result: model.InferenceResult
254
+ ) -> Self:
255
+ """Returns a new instance based on a given inference result."""
256
+
257
+ pae = inference_result.numerical_data['full_pae']
258
+ if not isinstance(pae, np.ndarray):
259
+ logging.info('%s', type(pae))
260
+ raise TypeError('pae should be a numpy array.')
261
+
262
+ contact_probs = inference_result.numerical_data['contact_probs']
263
+ if not isinstance(contact_probs, np.ndarray):
264
+ logging.info('%s', type(contact_probs))
265
+ raise TypeError('contact_probs should be a numpy array.')
266
+
267
+ struc = inference_result.predicted_structure
268
+ chain_ids = struc.chain_id.tolist()
269
+ atom_plddts = struc.atom_b_factor.tolist()
270
+ token_chain_ids = [
271
+ str(token_id)
272
+ for token_id in inference_result.metadata['token_chain_ids']
273
+ ]
274
+ token_res_ids = [
275
+ int(token_id) for token_id in inference_result.metadata['token_res_ids']
276
+ ]
277
+ return cls(
278
+ pae=pae,
279
+ token_chain_ids=token_chain_ids,
280
+ token_res_ids=token_res_ids,
281
+ atom_plddts=atom_plddts,
282
+ atom_chain_ids=chain_ids,
283
+ contact_probs=contact_probs,
284
+ )
285
+
286
+ @classmethod
287
+ def from_json(cls, json_string: str) -> Self:
288
+ """Returns a new instance from a given json string."""
289
+ return cls(**json.loads(json_string))
290
+
291
+ def to_json(self) -> str:
292
+ """Converts StructureConfidenceFull to json string."""
293
+ return json.dumps(self, cls=StructureConfidenceFullEncoder)
flax_model/alphafold3/model/confidences.py ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Functions for extracting and processing confidences from model outputs."""
4
+ import warnings
5
+
6
+ from absl import logging
7
+ from flax_model.alphafold3 import structure
8
+ from flax_model.alphafold3.constants import residue_names
9
+ from flax_model.alphafold3.cpp import mkdssp
10
+ import jax.numpy as jnp
11
+ import numpy as np
12
+ from scipy import spatial
13
+
14
+
15
+ # From Sander & Rost 1994 https://doi.org/10.1002/prot.340200303
16
+ MAX_ACCESSIBLE_SURFACE_AREA = {
17
+ 'ALA': 106.0,
18
+ 'ARG': 248.0,
19
+ 'ASN': 157.0,
20
+ 'ASP': 163.0,
21
+ 'CYS': 135.0,
22
+ 'GLN': 198.0,
23
+ 'GLU': 194.0,
24
+ 'GLY': 84.0,
25
+ 'HIS': 184.0,
26
+ 'ILE': 169.0,
27
+ 'LEU': 164.0,
28
+ 'LYS': 205.0,
29
+ 'MET': 188.0,
30
+ 'PHE': 197.0,
31
+ 'PRO': 136.0,
32
+ 'SER': 130.0,
33
+ 'THR': 142.0,
34
+ 'TRP': 227.0,
35
+ 'TYR': 222.0,
36
+ 'VAL': 142.0,
37
+ }
38
+
39
+ # Weights for ranking confidence.
40
+ _IPTM_WEIGHT = 0.8
41
+ _FRACTION_DISORDERED_WEIGHT = 0.5
42
+ _CLASH_PENALIZATION_WEIGHT = 100.0
43
+
44
+
45
+ def windowed_solvent_accessible_area(cif: str, window: int = 25) -> np.ndarray:
46
+ """Implementation of AlphaFold-RSA.
47
+
48
+ AlphaFold-RSA defined in https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9601767.
49
+
50
+ Args:
51
+ cif: Raw cif string.
52
+ window: The window over which to average accessible surface area
53
+
54
+ Returns:
55
+ An array of size num_res that predicts disorder by using windowed solvent
56
+ accessible surface area.
57
+ """
58
+ result = mkdssp.get_dssp(cif, calculate_surface_accessibility=True)
59
+ parse_row = False
60
+ rasa = []
61
+ for row in result.splitlines():
62
+ if parse_row:
63
+ aa = row[13:14]
64
+ if aa == '!':
65
+ continue
66
+ aa3 = residue_names.PROTEIN_COMMON_ONE_TO_THREE.get(aa, 'ALA')
67
+ max_acc = MAX_ACCESSIBLE_SURFACE_AREA[aa3]
68
+ acc = int(row[34:38])
69
+ norm_acc = acc / max_acc
70
+ if norm_acc > 1.0:
71
+ norm_acc = 1.0
72
+ rasa.append(norm_acc)
73
+ if row.startswith(' # RESIDUE'):
74
+ parse_row = True
75
+
76
+ half_w = (window - 1) // 2
77
+ pad_rasa = np.pad(rasa, (half_w, half_w), 'reflect')
78
+ rasa = np.convolve(pad_rasa, np.ones(window), 'valid') / window
79
+ return rasa
80
+
81
+
82
+ def fraction_disordered(
83
+ struc: structure.Structure, rasa_disorder_cutoff: float = 0.581
84
+ ) -> float:
85
+ """Compute fraction of protein residues that are disordered.
86
+
87
+ Args:
88
+ struc: A structure to compute rASA metrics on.
89
+ rasa_disorder_cutoff: The threshold at which residues are considered
90
+ disordered. Default value taken from
91
+ https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9601767.
92
+
93
+ Returns:
94
+ The fraction of protein residues that are disordered
95
+ (rasa > rasa_disorder_cutoff).
96
+ """
97
+ struc = struc.filter_to_entity_type(protein=True)
98
+ rasa = []
99
+ seq_rasa = {}
100
+ for chain_id, chain_seq in struc.chain_single_letter_sequence().items():
101
+ if chain_seq in seq_rasa:
102
+ # We assume that identical sequences have approximately similar rasa
103
+ # values to speed up the computation.
104
+ rasa.extend(seq_rasa[chain_seq])
105
+ continue
106
+ chain_struc = struc.filter(chain_id=chain_id)
107
+ # Rename the chain to 'A' as MKDSSP supports only single letter chain IDs.
108
+ chain_struc = chain_struc.rename_chain_ids(new_id_by_old_id={chain_id: 'A'})
109
+ try:
110
+ rasa_per_residue = windowed_solvent_accessible_area(
111
+ chain_struc.to_mmcif()
112
+ )
113
+ seq_rasa[chain_seq] = rasa_per_residue
114
+ rasa.extend(rasa_per_residue)
115
+ except (ValueError, RuntimeError) as e:
116
+ logging.warning('%s: rasa calculation failed: %s', struc.name, e)
117
+
118
+ if not rasa:
119
+ return 0.0
120
+ return np.mean(np.array(rasa) > rasa_disorder_cutoff)
121
+
122
+
123
+ def has_clash(
124
+ struc: structure.Structure,
125
+ cutoff_radius: float = 1.1,
126
+ min_clashes_for_overlap: int = 100,
127
+ min_fraction_for_overlap: float = 0.5,
128
+ ) -> bool:
129
+ """Determine whether the structure has at least one clashing chain.
130
+
131
+ A clashing chain is defined as having greater than 100 polymer atoms within
132
+ 1.1A of another polymer atom, or having more than 50% of the chain with
133
+ clashing atoms.
134
+
135
+ Args:
136
+ struc: A structure to get clash metrics for.
137
+ cutoff_radius: atom distances under this threshold are considered a clash.
138
+ min_clashes_for_overlap: The minimum number of atom-atom clashes for a chain
139
+ to be considered overlapping.
140
+ min_fraction_for_overlap: The minimum fraction of atoms within a chain that
141
+ are clashing for the chain to be considered overlapping.
142
+
143
+ Returns:
144
+ True if the structure has at least one clashing chain.
145
+ """
146
+ struc = struc.filter_to_entity_type(protein=True, rna=True, dna=True)
147
+ if not struc.chains:
148
+ return False
149
+ coords = struc.coords
150
+ coord_kdtree = spatial.cKDTree(coords)
151
+ clashes_per_atom = coord_kdtree.query_ball_point(
152
+ coords, p=2.0, r=cutoff_radius
153
+ )
154
+ per_atom_has_clash = np.zeros(len(coords), dtype=np.int32)
155
+ for atom_idx, clashing_indices in enumerate(clashes_per_atom):
156
+ for clashing_idx in clashing_indices:
157
+ if np.abs(struc.res_id[atom_idx] - struc.res_id[clashing_idx]) > 1 or (
158
+ struc.chain_id[atom_idx] != struc.chain_id[clashing_idx]
159
+ ):
160
+ per_atom_has_clash[atom_idx] = True
161
+ break
162
+ for chain_id in struc.chains:
163
+ mask = struc.chain_id == chain_id
164
+ num_atoms = np.sum(mask)
165
+ if num_atoms == 0:
166
+ continue
167
+ num_clashes = np.sum(per_atom_has_clash * mask)
168
+ frac_clashes = num_clashes / num_atoms
169
+ if (
170
+ num_clashes > min_clashes_for_overlap
171
+ or frac_clashes > min_fraction_for_overlap
172
+ ):
173
+ return True
174
+ return False
175
+
176
+
177
+ def get_ranking_score(
178
+ ptm: float, iptm: float, fraction_disordered_: float, has_clash_: bool
179
+ ) -> float:
180
+ # ipTM is NaN for single chain structures. Use pTM for such cases.
181
+ if np.isnan(iptm):
182
+ ptm_iptm_average = ptm
183
+ else:
184
+ ptm_iptm_average = _IPTM_WEIGHT * iptm + (1.0 - _IPTM_WEIGHT) * ptm
185
+ return (
186
+ ptm_iptm_average
187
+ + _FRACTION_DISORDERED_WEIGHT * fraction_disordered_
188
+ - _CLASH_PENALIZATION_WEIGHT * has_clash_
189
+ )
190
+
191
+
192
+ def rank_metric(
193
+ full_pde: jnp.ndarray | np.ndarray, contact_probs: jnp.ndarray | np.ndarray
194
+ ) -> jnp.ndarray | np.ndarray:
195
+ """Compute the metric that will be used to rank predictions, higher is better.
196
+
197
+ Args:
198
+ full_pde: A [num_samples, num_tokens,num_tokens] matrix of predicted
199
+ distance errors between pairs of tokens.
200
+ contact_probs: A [num_tokens, num_tokens] matrix consisting of the
201
+ probability of contact (<8A) that is returned from the distogram head.
202
+
203
+ Returns:
204
+ A scalar that can be used to rank (higher is better).
205
+ """
206
+ if not isinstance(full_pde, type(contact_probs)):
207
+ raise ValueError('full_pde and contact_probs must be of the same type.')
208
+
209
+ if isinstance(full_pde, np.ndarray):
210
+ sum_fn = np.sum
211
+ elif isinstance(full_pde, jnp.ndarray):
212
+ sum_fn = jnp.sum
213
+ else:
214
+ raise ValueError('full_pde must be a numpy array or a jax array.')
215
+ # It was found that taking the contact_map weighted average was better than
216
+ # just the predicted distance error on its own.
217
+ return -sum_fn(full_pde * contact_probs[None, :, :], axis=(-2, -1)) / (
218
+ sum_fn(contact_probs) + 1e-6
219
+ )
220
+
221
+
222
+ def weighted_mean(mask, value, axis):
223
+ return np.mean(mask * value, axis=axis) / (1e-8 + np.mean(mask, axis=axis))
224
+
225
+
226
+ def pde_single(
227
+ num_tokens: int,
228
+ asym_ids: np.ndarray,
229
+ full_pde: np.ndarray,
230
+ contact_probs: np.ndarray,
231
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
232
+ """Compute 1D PDE summaries.
233
+
234
+ Args:
235
+ num_tokens: The number of tokens (not including padding).
236
+ asym_ids: The asym_ids (array of shape num_tokens).
237
+ full_pde: A [num_samples, num_tokens, num_tokens] matrix of predicted
238
+ distance errors.
239
+ contact_probs: A [num_tokens, num_tokens] matrix consisting of the
240
+ probability of contact (<8A) that is returned from the distogram head.
241
+
242
+ Returns:
243
+ A tuple (ichain, xchain, full_chain) where:
244
+ `ichain` is a [num_samples, num_chains] matrix where the
245
+ value assigned to each chain is an average of the full PDE matrix over all
246
+ its within-chain interactions, weighted by `contact_probs`.
247
+ `xchain` is a [num_samples, num_chains] matrix where the
248
+ value assigned to each chain is an average of the full PDE matrix over all
249
+ its cross-chain interactions, weighted by `contact_probs`.
250
+ `full_chain` is a [num_samples, num_tokens] matrix where the
251
+ value assigned to each token is an average of it PDE against all tokens,
252
+ weighted by `contact_probs`.
253
+ """
254
+
255
+ full_pde = full_pde[:, :num_tokens, :num_tokens]
256
+ contact_probs = contact_probs[:num_tokens, :num_tokens]
257
+ asym_ids = asym_ids[:num_tokens]
258
+ unique_asym_ids = np.unique(asym_ids)
259
+ num_chains = len(unique_asym_ids)
260
+ num_samples = full_pde.shape[0]
261
+
262
+ asym_ids = asym_ids[None]
263
+ contact_probs = contact_probs[None]
264
+
265
+ ichain = np.zeros((num_samples, num_chains))
266
+ xchain = np.zeros((num_samples, num_chains))
267
+
268
+ for idx, asym_id in enumerate(unique_asym_ids):
269
+ my_asym_id = asym_ids == asym_id
270
+ imask = my_asym_id[:, :, None] * my_asym_id[:, None, :]
271
+ xmask = my_asym_id[:, :, None] * ~my_asym_id[:, None, :]
272
+ imask = imask * contact_probs
273
+ xmask = xmask * contact_probs
274
+ ichain[:, idx] = weighted_mean(mask=imask, value=full_pde, axis=(-2, -1))
275
+ xchain[:, idx] = weighted_mean(mask=xmask, value=full_pde, axis=(-2, -1))
276
+
277
+ full_chain = weighted_mean(mask=contact_probs, value=full_pde, axis=(-1,))
278
+
279
+ return ichain, xchain, full_chain
280
+
281
+
282
+ def chain_pair_pde(
283
+ num_tokens: int, asym_ids: np.ndarray, full_pde: np.ndarray
284
+ ) -> tuple[np.ndarray, np.ndarray]:
285
+ """Compute predicted distance errors for all pairs of chains.
286
+
287
+ Args:
288
+ num_tokens: The number of tokens (not including padding).
289
+ asym_ids: The asym_ids (array of shape num_tokens).
290
+ full_pde: A [num_samples, num_tokens, num_tokens] matrix of predicted
291
+ distance errors.
292
+
293
+ Returns:
294
+ chain_pair_pred_err_mean - a [num_chains, num_chains] matrix with average
295
+ per chain-pair predicted distance error.
296
+ chain_pair_pred_err_min - a [num_chains, num_chains] matrix with min
297
+ per chain-pair predicted distance error.
298
+ """
299
+ full_pde = full_pde[:, :num_tokens, :num_tokens]
300
+ asym_ids = asym_ids[:num_tokens]
301
+ unique_asym_ids = np.unique(asym_ids)
302
+ num_chains = len(unique_asym_ids)
303
+ num_samples = full_pde.shape[0]
304
+ chain_pair_pred_err_mean = np.zeros((num_samples, num_chains, num_chains))
305
+ chain_pair_pred_err_min = np.zeros((num_samples, num_chains, num_chains))
306
+
307
+ for idx1, asym_id_1 in enumerate(unique_asym_ids):
308
+ subset = full_pde[:, asym_ids == asym_id_1, :]
309
+ for idx2, asym_id_2 in enumerate(unique_asym_ids):
310
+ subsubset = subset[:, :, asym_ids == asym_id_2]
311
+ chain_pair_pred_err_mean[:, idx1, idx2] = np.mean(subsubset, axis=(1, 2))
312
+ chain_pair_pred_err_min[:, idx1, idx2] = np.min(subsubset, axis=(1, 2))
313
+ return chain_pair_pred_err_mean, chain_pair_pred_err_min
314
+
315
+
316
+ def weighted_nanmean(
317
+ value: np.ndarray, mask: np.ndarray, axis: int
318
+ ) -> np.ndarray:
319
+ """Nan-mean with weighting -- empty slices return NaN."""
320
+ assert mask.shape == value.shape
321
+ assert not np.isnan(mask).all()
322
+
323
+ nan_idxs = np.where(np.isnan(value))
324
+ # Need to NaN the mask to get the correct denominator weighting.
325
+ mask_with_nan = mask.copy()
326
+ mask_with_nan[nan_idxs] = np.nan
327
+ with warnings.catch_warnings():
328
+ # Mean of empty slice is ok and should return a NaN.
329
+ warnings.filterwarnings(action='ignore', message='Mean of empty slice')
330
+ warnings.filterwarnings(
331
+ action='ignore', message='invalid value encountered in (scalar )?divide'
332
+ )
333
+ return np.nanmean(value * mask_with_nan, axis=axis) / np.nanmean(
334
+ mask_with_nan, axis=axis
335
+ )
336
+
337
+
338
+ def chain_pair_pae(
339
+ *,
340
+ num_tokens: int,
341
+ asym_ids: np.ndarray,
342
+ full_pae: np.ndarray,
343
+ mask: np.ndarray | None = None,
344
+ contact_probs: np.ndarray | None = None,
345
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
346
+ """Compute predicted errors for all pairs of chains.
347
+
348
+ Args:
349
+ num_tokens: The number of tokens (not including padding).
350
+ asym_ids: The asym_ids (array of shape num_tokens).
351
+ full_pae: A [num_samples, num_tokens, num_tokens] matrix of predicted
352
+ errors.
353
+ mask: A [num_tokens, num_tokens] mask matrix.
354
+ contact_probs: A [num_tokens, num_tokens] matrix consisting of the
355
+ probability of contact (<8A) that is returned from the distogram head.
356
+
357
+ Returns:
358
+ chain_pair_pred_err_mean - a [num_chains, num_chains] matrix with average
359
+ per chain-pair predicted error.
360
+ """
361
+ if mask is None:
362
+ mask = np.ones(shape=full_pae.shape[1:], dtype=bool)
363
+ if contact_probs is None:
364
+ contact_probs = np.ones(shape=full_pae.shape[1:], dtype=float)
365
+ assert mask.shape == full_pae.shape[1:]
366
+
367
+ full_pae = full_pae[:, :num_tokens, :num_tokens]
368
+ mask = mask[:num_tokens, :num_tokens]
369
+ asym_ids = asym_ids[:num_tokens]
370
+ contact_probs = contact_probs[:num_tokens, :num_tokens]
371
+ unique_asym_ids = np.unique(asym_ids)
372
+ num_chains = len(unique_asym_ids)
373
+ num_samples = full_pae.shape[0]
374
+ chain_pair_pred_err_mean = np.zeros((num_samples, num_chains, num_chains))
375
+ chain_pair_pred_err_min = np.zeros((num_samples, num_chains, num_chains))
376
+
377
+ for idx1, asym_id_1 in enumerate(unique_asym_ids):
378
+ subset = full_pae[:, asym_ids == asym_id_1, :]
379
+ subset_mask = mask[asym_ids == asym_id_1, :]
380
+ subset_contact_probs = contact_probs[asym_ids == asym_id_1, :]
381
+ for idx2, asym_id_2 in enumerate(unique_asym_ids):
382
+ subsubset = subset[:, :, asym_ids == asym_id_2]
383
+ subsubset_mask = subset_mask[:, asym_ids == asym_id_2]
384
+ subsubset_contact_probs = subset_contact_probs[:, asym_ids == asym_id_2]
385
+ (flat_mask_idxs,) = np.where(subsubset_mask.flatten() > 0)
386
+ flat_subsubset = subsubset.reshape([num_samples, -1])
387
+ flat_contact_probs = subsubset_contact_probs.flatten()
388
+ # A ligand chain will have no valid frames if it contains fewer than
389
+ # three non-colinear atoms (e.g. a sodium ion).
390
+ if not flat_mask_idxs.size:
391
+ chain_pair_pred_err_mean[:, idx1, idx2] = np.nan
392
+ chain_pair_pred_err_min[:, idx1, idx2] = np.nan
393
+ else:
394
+ chain_pair_pred_err_min[:, idx1, idx2] = np.min(
395
+ flat_subsubset[:, flat_mask_idxs], axis=1
396
+ )
397
+ chain_pair_pred_err_mean[:, idx1, idx2] = weighted_mean(
398
+ mask=flat_contact_probs[flat_mask_idxs],
399
+ value=flat_subsubset[:, flat_mask_idxs],
400
+ axis=-1,
401
+ )
402
+ return chain_pair_pred_err_mean, chain_pair_pred_err_min, unique_asym_ids
403
+
404
+
405
+ def reduce_chain_pair(
406
+ *,
407
+ chain_pair_met: np.ndarray,
408
+ num_chain_tokens: np.ndarray,
409
+ agg_over_col: bool,
410
+ agg_type: str,
411
+ weight_method: str,
412
+ ) -> tuple[np.ndarray, np.ndarray]:
413
+ """Compute 1D summaries from a chain-pair summary.
414
+
415
+ Args:
416
+ chain_pair_met: A [num_samples, num_chains, num_chains] aggregate matrix.
417
+ num_chain_tokens: A [num_chains] array of number of tokens for each chain.
418
+ Used for 'per_token' weighting.
419
+ agg_over_col: Whether to aggregate the PAE over rows (i.e. average error
420
+ when aligned to me) or columns (i.e. my average error when aligned to all
421
+ others.)
422
+ agg_type: The type of aggregation to use, 'mean' or 'min'.
423
+ weight_method: The method to use for weighting the PAE, 'per_token' or
424
+ 'per_chain'.
425
+
426
+ Returns:
427
+ A tuple (ichain, xchain) where:
428
+ `ichain` is a [num_samples, num_chains] matrix where the
429
+ value assigned to each chain is an average of the full PAE matrix over all
430
+ its within-chain interactions, weighted by `contact_probs`.
431
+ `xchain` is a [num_samples, num_chains] matrix where the
432
+ value assigned to each chain is an average of the full PAE matrix over all
433
+ its cross-chain interactions, weighted by `contact_probs`.
434
+ """
435
+ num_samples, num_chains, _ = chain_pair_met.shape
436
+
437
+ ichain = chain_pair_met.diagonal(axis1=-2, axis2=-1)
438
+
439
+ if weight_method == 'per_chain':
440
+ chain_weight = np.ones((num_chains,), dtype=float)
441
+ elif weight_method == 'per_token':
442
+ chain_weight = num_chain_tokens
443
+ else:
444
+ raise ValueError(f'Unknown weight method: {weight_method}')
445
+
446
+ if agg_over_col:
447
+ agg_axis = -1
448
+ else:
449
+ agg_axis = -2
450
+
451
+ if agg_type == 'mean':
452
+ weight = np.ones((num_samples, num_chains, num_chains), dtype=float)
453
+ weight -= np.eye(num_chains, dtype=float)
454
+ weight *= chain_weight[None] * chain_weight[:, None]
455
+ xchain = weighted_nanmean(chain_pair_met, mask=weight, axis=agg_axis)
456
+ elif agg_type == 'min':
457
+ is_self = np.eye(num_chains)
458
+ with warnings.catch_warnings():
459
+ # Min over empty slice is ok and should return a NaN.
460
+ warnings.filterwarnings('ignore', message='All-NaN slice encountered')
461
+ xchain = np.nanmin(chain_pair_met + 1e8 * is_self, axis=agg_axis)
462
+ else:
463
+ raise ValueError(f'Unknown aggregation method: {agg_type}')
464
+
465
+ return ichain, xchain
466
+
467
+
468
+ def pae_metrics(
469
+ num_tokens: int,
470
+ asym_ids: np.ndarray,
471
+ full_pae: np.ndarray,
472
+ mask: np.ndarray,
473
+ contact_probs: np.ndarray,
474
+ tm_adjusted_pae: np.ndarray,
475
+ ):
476
+ """PAE aggregate metrics."""
477
+ assert mask.shape == full_pae.shape[1:]
478
+ assert contact_probs.shape == full_pae.shape[1:]
479
+
480
+ chain_pair_contact_weighted, _, unique_asym_ids = chain_pair_pae(
481
+ num_tokens=num_tokens,
482
+ asym_ids=asym_ids,
483
+ full_pae=full_pae,
484
+ mask=mask,
485
+ contact_probs=contact_probs,
486
+ )
487
+
488
+ ret = {}
489
+ ret['chain_pair_pae_mean'], ret['chain_pair_pae_min'], _ = chain_pair_pae(
490
+ num_tokens=num_tokens,
491
+ asym_ids=asym_ids,
492
+ full_pae=full_pae,
493
+ mask=mask,
494
+ )
495
+ chain_pair_iptm = np.stack(
496
+ [
497
+ chain_pairwise_predicted_tm_scores(
498
+ tm_adjusted_pae=sample_tm_adjusted_pae[:num_tokens],
499
+ asym_id=asym_ids[:num_tokens],
500
+ pair_mask=mask[:num_tokens, :num_tokens],
501
+ )
502
+ for sample_tm_adjusted_pae in tm_adjusted_pae
503
+ ],
504
+ axis=0,
505
+ )
506
+
507
+ num_chain_tokens = np.array(
508
+ [sum(asym_ids == asym_id) for asym_id in unique_asym_ids]
509
+ )
510
+
511
+ def reduce_chain_pair_fn(chain_pair: np.ndarray):
512
+ def inner(agg_over_col):
513
+ ichain_pae, xchain_pae = reduce_chain_pair(
514
+ num_chain_tokens=num_chain_tokens,
515
+ chain_pair_met=chain_pair,
516
+ agg_over_col=agg_over_col,
517
+ agg_type='mean',
518
+ weight_method='per_chain',
519
+ )
520
+ return ichain_pae, xchain_pae
521
+
522
+ ichain, xchain_row_agg = inner(False)
523
+ _, xchain_col_agg = inner(True)
524
+ with warnings.catch_warnings():
525
+ # Mean of empty slice is ok and should return a NaN.
526
+ warnings.filterwarnings(action='ignore', message='Mean of empty slice')
527
+ xchain = np.nanmean(
528
+ np.stack([xchain_row_agg, xchain_col_agg], axis=0), axis=0
529
+ )
530
+ return ichain, xchain
531
+
532
+ pae_ichain, pae_xchain = reduce_chain_pair_fn(chain_pair_contact_weighted)
533
+ iptm_ichain, iptm_xchain = reduce_chain_pair_fn(chain_pair_iptm)
534
+
535
+ ret.update({
536
+ 'chain_pair_iptm': chain_pair_iptm,
537
+ 'iptm_ichain': iptm_ichain,
538
+ 'iptm_xchain': iptm_xchain,
539
+ 'pae_ichain': pae_ichain,
540
+ 'pae_xchain': pae_xchain,
541
+ })
542
+
543
+ return ret
544
+
545
+
546
+ def get_iptm_xchain(chain_pair_iptm: np.ndarray) -> np.ndarray:
547
+ """Cross chain aggregate ipTM."""
548
+ num_samples, num_chains, _ = chain_pair_iptm.shape
549
+ weight = np.ones((num_samples, num_chains, num_chains), dtype=float)
550
+ weight -= np.eye(num_chains, dtype=float)
551
+ xchain_row_agg = weighted_nanmean(chain_pair_iptm, mask=weight, axis=-2)
552
+ xchain_col_agg = weighted_nanmean(chain_pair_iptm, mask=weight, axis=-1)
553
+ with warnings.catch_warnings():
554
+ # Mean of empty slice is ok and should return a NaN.
555
+ warnings.filterwarnings(action='ignore', message='Mean of empty slice')
556
+ iptm_xchain = np.nanmean(
557
+ np.stack([xchain_row_agg, xchain_col_agg], axis=0), axis=0
558
+ )
559
+ return iptm_xchain
560
+
561
+
562
+ def predicted_tm_score(
563
+ tm_adjusted_pae: np.ndarray,
564
+ pair_mask: np.ndarray,
565
+ asym_id: np.ndarray,
566
+ interface: bool = False,
567
+ ) -> float:
568
+ """Computes predicted TM alignment or predicted interface TM alignment score.
569
+
570
+ Args:
571
+ tm_adjusted_pae: [num_res, num_res] Relevant tensor for computing TMScore
572
+ values.
573
+ pair_mask: A [num_res, num_res] mask. The TM score will only aggregate over
574
+ masked-on entries.
575
+ asym_id: [num_res] asymmetric unit ID (the chain ID). Only needed for ipTM
576
+ calculation, i.e. when interface=True.
577
+ interface: If True, the interface predicted TM score is computed. If False,
578
+ the predicted TM score without any residue pair restrictions is computed.
579
+
580
+ Returns:
581
+ score: pTM or ipTM score.
582
+ """
583
+ num_tokens, _ = tm_adjusted_pae.shape
584
+ if tm_adjusted_pae.shape != (num_tokens, num_tokens):
585
+ raise ValueError(
586
+ f'Bad tm_adjusted_pae shape, expected ({num_tokens, num_tokens}), got '
587
+ f'{tm_adjusted_pae.shape}.'
588
+ )
589
+
590
+ if pair_mask.shape != (num_tokens, num_tokens):
591
+ raise ValueError(
592
+ f'Bad pair_mask shape, expected ({num_tokens, num_tokens}), got '
593
+ f'{pair_mask.shape}.'
594
+ )
595
+ if pair_mask.dtype != bool:
596
+ raise TypeError(f'Bad pair mask type, expected bool, got {pair_mask.dtype}')
597
+ if asym_id.shape[0] != num_tokens:
598
+ raise ValueError(
599
+ f'Bad asym_id shape, expected ({num_tokens},), got {asym_id.shape}.'
600
+ )
601
+
602
+ # Create pair mask.
603
+ if interface:
604
+ pair_mask = pair_mask * (asym_id[:, None] != asym_id[None, :])
605
+
606
+ # Ions and other ligands with colinear atoms have ill-defined frames.
607
+ if pair_mask.sum() == 0:
608
+ return np.nan
609
+
610
+ normed_residue_mask = pair_mask / (
611
+ 1e-8 + np.sum(pair_mask, axis=-1, keepdims=True)
612
+ )
613
+ per_alignment = np.sum(tm_adjusted_pae * normed_residue_mask, axis=-1)
614
+ return per_alignment.max()
615
+
616
+
617
+ def chain_pairwise_predicted_tm_scores(
618
+ tm_adjusted_pae: np.ndarray,
619
+ pair_mask: np.ndarray,
620
+ asym_id: np.ndarray,
621
+ ) -> np.ndarray:
622
+ """Compute predicted TM (pTM) between each pair of chains independently.
623
+
624
+ Args:
625
+ tm_adjusted_pae: [num_res, num_res] Relevant tensor for computing TMScore
626
+ values.
627
+ pair_mask: A [num_res, num_res] mask specifying which frames are valid.
628
+ Invalid frames can be the result of chains with not enough atoms (e.g.
629
+ ions).
630
+ asym_id: [num_res] asymmetric unit ID (the chain ID).
631
+
632
+ Returns:
633
+ A [num_chains, num_chains] matrix, where row i, column j indicates the
634
+ predicted TM-score for the interface between chain i and chain j.
635
+ """
636
+ unique_chains = list(np.unique(asym_id))
637
+ num_chains = len(unique_chains)
638
+ all_pairs_iptms = np.zeros((num_chains, num_chains))
639
+ for i, chain_i in enumerate(unique_chains):
640
+ chain_i_mask = asym_id == chain_i
641
+ for j, chain_j in enumerate(unique_chains[i:]):
642
+ chain_j_mask = asym_id == chain_j
643
+ mask = chain_i_mask | chain_j_mask
644
+ (indices,) = np.where(mask)
645
+ is_interface = chain_i != chain_j
646
+ indices = np.ix_(indices, indices)
647
+ iptm = predicted_tm_score(
648
+ tm_adjusted_pae=tm_adjusted_pae[indices],
649
+ pair_mask=pair_mask[indices],
650
+ asym_id=asym_id[mask],
651
+ interface=is_interface,
652
+ )
653
+ all_pairs_iptms[i, i + j] = iptm
654
+ all_pairs_iptms[i + j, i] = iptm
655
+ return all_pairs_iptms
flax_model/alphafold3/model/data3.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Protein features that are computed from parsed mmCIF objects."""
4
+
5
+ from collections.abc import Mapping, MutableMapping
6
+ import datetime
7
+ from typing import TypeAlias
8
+
9
+ from flax_model.alphafold3.constants import residue_names
10
+ from flax_model.alphafold3.cpp import msa_profile
11
+ from flax_model.alphafold3.model import protein_data_processing
12
+ import numpy as np
13
+
14
+
15
+ FeatureDict: TypeAlias = Mapping[str, np.ndarray]
16
+ # MutableFeatureDict: TypeAlias = MutableMapping[str, np.ndarray]
17
+
18
+
19
+ # def fix_features(msa_features: MutableFeatureDict) -> MutableFeatureDict:
20
+ # """Renames the deletion_matrix feature."""
21
+ # msa_features['deletion_matrix'] = msa_features.pop('deletion_matrix_int')
22
+ # return msa_features
23
+
24
+
25
+ def get_profile_features(
26
+ msa: np.ndarray, deletion_matrix: np.ndarray
27
+ ) -> FeatureDict:
28
+ """Returns the MSA profile and deletion_mean features."""
29
+ num_restypes = residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP
30
+ profile = msa_profile.compute_msa_profile(
31
+ msa=msa, num_residue_types=num_restypes
32
+ )
33
+
34
+ return {
35
+ 'profile': profile.astype(np.float32),
36
+ 'deletion_mean': np.mean(deletion_matrix, axis=0),
37
+ }
38
+
39
+
40
+ def fix_template_features(
41
+ template_features: FeatureDict, num_res: int
42
+ ) -> FeatureDict:
43
+ """Convert template features to AlphaFold 3 format.
44
+
45
+ Args:
46
+ template_features: Template features for the protein.
47
+ num_res: The length of the amino acid sequence of the protein.
48
+
49
+ Returns:
50
+ Updated template_features for the chain.
51
+ """
52
+ if not template_features['template_aatype'].shape[0]:
53
+ template_features = empty_template_features(num_res)
54
+ else:
55
+ template_release_timestamp = [
56
+ _get_timestamp(x.decode('utf-8'))
57
+ for x in template_features['template_release_date']
58
+ ]
59
+
60
+ # Convert from atom37 to dense atom
61
+ dense_atom_indices = np.take(
62
+ protein_data_processing.PROTEIN_AATYPE_DENSE_ATOM_TO_ATOM37,
63
+ template_features['template_aatype'],
64
+ axis=0,
65
+ )
66
+
67
+ atom_mask = np.take_along_axis(
68
+ template_features['template_all_atom_masks'], dense_atom_indices, axis=2
69
+ )
70
+ atom_positions = np.take_along_axis(
71
+ template_features['template_all_atom_positions'],
72
+ dense_atom_indices[..., None],
73
+ axis=2,
74
+ )
75
+ atom_positions *= atom_mask[..., None]
76
+
77
+ template_features = {
78
+ 'template_aatype': template_features['template_aatype'],
79
+ 'template_atom_mask': atom_mask.astype(np.int32),
80
+ 'template_atom_positions': atom_positions.astype(np.float32),
81
+ 'template_domain_names': np.array(
82
+ template_features['template_domain_names'], dtype=object
83
+ ),
84
+ 'template_release_timestamp': np.array(
85
+ template_release_timestamp, dtype=np.float32
86
+ ),
87
+ }
88
+ return template_features
89
+
90
+
91
+ def empty_template_features(num_res: int) -> FeatureDict:
92
+ """Creates a fully masked out template features to allow padding to work.
93
+
94
+ Args:
95
+ num_res: The length of the target chain.
96
+
97
+ Returns:
98
+ Empty template features for the chain.
99
+ """
100
+ template_features = {
101
+ 'template_aatype': np.zeros(num_res, dtype=np.int32)[None, ...],
102
+ 'template_atom_mask': np.zeros(
103
+ (num_res, protein_data_processing.NUM_DENSE), dtype=np.int32
104
+ )[None, ...],
105
+ 'template_atom_positions': np.zeros(
106
+ (num_res, protein_data_processing.NUM_DENSE, 3), dtype=np.float32
107
+ )[None, ...],
108
+ 'template_domain_names': np.array([b''], dtype=object),
109
+ 'template_release_timestamp': np.array([0.0], dtype=np.float32),
110
+ }
111
+ return template_features
112
+
113
+
114
+ def _get_timestamp(date_str: str):
115
+ dt = datetime.datetime.fromisoformat(date_str)
116
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
117
+ return dt.timestamp()
flax_model/alphafold3/model/data_constants.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Constants shared across modules in the AlphaFold data pipeline."""
4
+
5
+ from flax_model.alphafold3.constants import residue_names
6
+
7
+ MSA_GAP_IDX = residue_names.PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP.index(
8
+ '-'
9
+ )
10
+
11
+ # Feature groups.
12
+ NUM_SEQ_NUM_RES_MSA_FEATURES = ('msa', 'msa_mask', 'deletion_matrix')
13
+ NUM_SEQ_MSA_FEATURES = ('msa_species_identifiers',)
14
+ TEMPLATE_FEATURES = (
15
+ 'template_aatype',
16
+ 'template_atom_positions',
17
+ 'template_atom_mask',
18
+ )
19
+ MSA_PAD_VALUES = {'msa': MSA_GAP_IDX, 'msa_mask': 1, 'deletion_matrix': 0}
flax_model/alphafold3/model/feat_batch.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Batch dataclass."""
4
+ import dataclasses
5
+ from typing import Self
6
+
7
+ from flax_model.alphafold3.model import features
8
+ #import chex
9
+ import jax
10
+
11
+
12
+ @dataclasses.dataclass(frozen=True)
13
+ class Batch:
14
+ """Dataclass containing batch."""
15
+
16
+ msa: features.MSA
17
+ templates: features.Templates
18
+ token_features: features.TokenFeatures
19
+ ref_structure: features.RefStructure
20
+ predicted_structure_info: features.PredictedStructureInfo
21
+ polymer_ligand_bond_info: features.PolymerLigandBondInfo
22
+ ligand_ligand_bond_info: features.LigandLigandBondInfo
23
+ pseudo_beta_info: features.PseudoBetaInfo
24
+ atom_cross_att: features.AtomCrossAtt
25
+ convert_model_output: features.ConvertModelOutput
26
+ frames: features.Frames
27
+
28
+ @property
29
+ def num_res(self) -> int:
30
+ return self.token_features.aatype.shape[-1]
31
+
32
+ @classmethod
33
+ def from_data_dict(cls, batch: features.BatchDict) -> Self:
34
+ """Construct batch object from dictionary."""
35
+ return cls(
36
+ msa=features.MSA.from_data_dict(batch),
37
+ templates=features.Templates.from_data_dict(batch),
38
+ token_features=features.TokenFeatures.from_data_dict(batch),
39
+ ref_structure=features.RefStructure.from_data_dict(batch),
40
+ predicted_structure_info=features.PredictedStructureInfo.from_data_dict(
41
+ batch
42
+ ),
43
+ polymer_ligand_bond_info=features.PolymerLigandBondInfo.from_data_dict(
44
+ batch
45
+ ),
46
+ ligand_ligand_bond_info=features.LigandLigandBondInfo.from_data_dict(
47
+ batch
48
+ ),
49
+ pseudo_beta_info=features.PseudoBetaInfo.from_data_dict(batch),
50
+ atom_cross_att=features.AtomCrossAtt.from_data_dict(batch),
51
+ convert_model_output=features.ConvertModelOutput.from_data_dict(batch),
52
+ frames=features.Frames.from_data_dict(batch),
53
+ )
54
+
55
+ def as_data_dict(self) -> features.BatchDict:
56
+ """Converts batch object to dictionary."""
57
+ output = {
58
+ **self.msa.as_data_dict(),
59
+ **self.templates.as_data_dict(),
60
+ **self.token_features.as_data_dict(),
61
+ **self.ref_structure.as_data_dict(),
62
+ **self.predicted_structure_info.as_data_dict(),
63
+ **self.polymer_ligand_bond_info.as_data_dict(),
64
+ **self.ligand_ligand_bond_info.as_data_dict(),
65
+ **self.pseudo_beta_info.as_data_dict(),
66
+ **self.atom_cross_att.as_data_dict(),
67
+ **self.convert_model_output.as_data_dict(),
68
+ **self.frames.as_data_dict(),
69
+ }
70
+ return output
71
+
72
+
73
+ jax.tree_util.register_dataclass(
74
+ Batch,
75
+ data_fields=[f.name for f in dataclasses.fields(Batch)],
76
+ meta_fields=[],
77
+ )
flax_model/alphafold3/model/features.py ADDED
@@ -0,0 +1,2159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Data-side of the input features processing."""
4
+
5
+ import dataclasses
6
+ import datetime
7
+ import itertools
8
+ from typing import Any, Self, TypeAlias
9
+
10
+ from absl import logging
11
+ from flax_model.alphafold3 import structure
12
+ from flax_model.alphafold3.common import folding_input
13
+ from flax_model.alphafold3.constants import chemical_components
14
+ from flax_model.alphafold3.constants import mmcif_names
15
+ from flax_model.alphafold3.constants import periodic_table
16
+ from flax_model.alphafold3.constants import residue_names
17
+ from flax_model.alphafold3.cpp import cif_dict
18
+ from flax_model.alphafold3.data import msa as msa_module
19
+ from flax_model.alphafold3.data import templates
20
+ from flax_model.alphafold3.data.tools import rdkit_utils
21
+ from flax_model.alphafold3.model import data3
22
+ from flax_model.alphafold3.model import data_constants
23
+ from flax_model.alphafold3.model import merging_features
24
+ from flax_model.alphafold3.model import msa_pairing
25
+ from flax_model.alphafold3.model.atom_layout import atom_layout
26
+ from flax_model.alphafold3.structure import chemical_components as struc_chem_comps
27
+ import jax
28
+ import jax.numpy as jnp
29
+ import numpy as np
30
+ from rdkit import Chem
31
+
32
+
33
+ xnp_ndarray: TypeAlias = np.ndarray | jnp.ndarray # pylint: disable=invalid-name
34
+ BatchDict: TypeAlias = dict[str, xnp_ndarray]
35
+
36
+ _STANDARD_RESIDUES = frozenset({
37
+ *residue_names.PROTEIN_TYPES_WITH_UNKNOWN,
38
+ *residue_names.NUCLEIC_TYPES_WITH_2_UNKS,
39
+ })
40
+
41
+
42
+ @dataclasses.dataclass(frozen=True)
43
+ class PaddingShapes:
44
+ num_tokens: int
45
+ msa_size: int
46
+ num_chains: int
47
+ num_templates: int
48
+ num_atoms: int
49
+
50
+
51
+ def _pad_to(
52
+ arr: np.ndarray, shape: tuple[int | None, ...], **kwargs
53
+ ) -> np.ndarray:
54
+ """Pads an array to a given shape. Wrapper around np.pad().
55
+
56
+ Args:
57
+ arr: numpy array to pad
58
+ shape: target shape, use None for axes that should stay the same
59
+ **kwargs: additional args for np.pad, e.g. constant_values=-1
60
+
61
+ Returns:
62
+ the padded array
63
+
64
+ Raises:
65
+ ValueError if arr and shape have a different number of axes.
66
+ """
67
+ if arr.ndim != len(shape):
68
+ raise ValueError(
69
+ f'arr and shape have different number of axes. {arr.shape=}, {shape=}'
70
+ )
71
+
72
+ num_pad = []
73
+ for axis, width in enumerate(shape):
74
+ if width is None:
75
+ num_pad.append((0, 0))
76
+ else:
77
+ if width >= arr.shape[axis]:
78
+ num_pad.append((0, width - arr.shape[axis]))
79
+ else:
80
+ raise ValueError(
81
+ f'Can not pad to a smaller shape. {arr.shape=}, {shape=}'
82
+ )
83
+ padded_arr = np.pad(arr, pad_width=num_pad, **kwargs)
84
+ return padded_arr
85
+
86
+
87
+ def _unwrap(obj):
88
+ """Unwrap an object from a zero-dim np.ndarray."""
89
+ if isinstance(obj, np.ndarray) and obj.ndim == 0:
90
+ return obj.item()
91
+ else:
92
+ return obj
93
+
94
+
95
+ @dataclasses.dataclass(frozen=True)
96
+ class Chains:
97
+ chain_id: np.ndarray
98
+ asym_id: np.ndarray
99
+ entity_id: np.ndarray
100
+ sym_id: np.ndarray
101
+
102
+
103
+ jax.tree_util.register_dataclass(
104
+ Chains,
105
+ data_fields=[f.name for f in dataclasses.fields(Chains)],
106
+ meta_fields=[],
107
+ )
108
+
109
+
110
+ def _compute_asym_entity_and_sym_id(
111
+ all_tokens: atom_layout.AtomLayout,
112
+ ) -> Chains:
113
+ """Compute asym_id, entity_id and sym_id.
114
+
115
+ Args:
116
+ all_tokens: atom layout containing a representative atom for each token.
117
+
118
+ Returns:
119
+ A Chains object
120
+ """
121
+
122
+ # Find identical sequences and assign entity_id and sym_id to every chain.
123
+ seq_to_entity_id_sym_id = {}
124
+ seen_chain_ids = set()
125
+ chain_ids = []
126
+ asym_ids = []
127
+ entity_ids = []
128
+ sym_ids = []
129
+ for chain_id in all_tokens.chain_id:
130
+ if chain_id not in seen_chain_ids:
131
+ asym_id = len(seen_chain_ids) + 1
132
+ seen_chain_ids.add(chain_id)
133
+ seq = ','.join(all_tokens.res_name[all_tokens.chain_id == chain_id])
134
+ if seq not in seq_to_entity_id_sym_id:
135
+ entity_id = len(seq_to_entity_id_sym_id) + 1
136
+ sym_id = 1
137
+ else:
138
+ entity_id, sym_id = seq_to_entity_id_sym_id[seq]
139
+ sym_id += 1
140
+ seq_to_entity_id_sym_id[seq] = (entity_id, sym_id)
141
+
142
+ chain_ids.append(chain_id)
143
+ asym_ids.append(asym_id)
144
+ entity_ids.append(entity_id)
145
+ sym_ids.append(sym_id)
146
+
147
+ return Chains(
148
+ chain_id=np.array(chain_ids),
149
+ asym_id=np.array(asym_ids),
150
+ entity_id=np.array(entity_ids),
151
+ sym_id=np.array(sym_ids),
152
+ )
153
+
154
+
155
+ def tokenizer(
156
+ flat_output_layout: atom_layout.AtomLayout,
157
+ ccd: chemical_components.Ccd,
158
+ max_atoms_per_token: int,
159
+ flatten_non_standard_residues: bool,
160
+ logging_name: str,
161
+ ) -> tuple[atom_layout.AtomLayout, atom_layout.AtomLayout, np.ndarray]:
162
+ """Maps a flat atom layout to tokens for evoformer.
163
+
164
+ Creates the evoformer tokens as one token per polymer residue and one token
165
+ per ligand atom. The tokens are represented as AtomLayouts all_tokens
166
+ (1 representative atom per token) atoms per residue, and
167
+ all_token_atoms_layout (num_tokens, max_atoms_per_token). The atoms in a
168
+ residue token use the layout of the corresponding CCD entry
169
+
170
+ Args:
171
+ flat_output_layout: flat AtomLayout containing all atoms that the model
172
+ wants to predict.
173
+ ccd: The chemical components dictionary.
174
+ max_atoms_per_token: number of slots per token.
175
+ flatten_non_standard_residues: whether to flatten non-standard residues,
176
+ i.e. whether to use one token per atom for non-standard residues.
177
+ logging_name: logging name for debugging (usually the mmcif_id).
178
+
179
+ Returns:
180
+ A tuple (all_tokens, all_tokens_atoms_layout) with
181
+ all_tokens: AtomLayout shape (num_tokens,) containing one representative
182
+ atom per token.
183
+ all_token_atoms_layout: AtomLayout with shape
184
+ (num_tokens, max_atoms_per_token) containing all atoms per token.
185
+ standard_token_idxs: The token index that each token would have if not
186
+ flattening non standard resiudes.
187
+ """
188
+ # Select the representative atom for each token.
189
+ token_idxs = []
190
+ single_atom_token = []
191
+ standard_token_idxs = []
192
+ current_standard_token_id = 0
193
+ # Iterate over residues, and provide a group_iter over the atoms of each
194
+ # residue.
195
+ for key, group_iter in itertools.groupby(
196
+ zip(
197
+ flat_output_layout.chain_type,
198
+ flat_output_layout.chain_id,
199
+ flat_output_layout.res_id,
200
+ flat_output_layout.res_name,
201
+ flat_output_layout.atom_name,
202
+ np.arange(flat_output_layout.shape[0]),
203
+ ),
204
+ key=lambda x: x[:3],
205
+ ):
206
+
207
+ # Get chain type and chain id of this residue
208
+ chain_type, chain_id, _ = key
209
+
210
+ # Get names and global idxs for all atoms of this residue
211
+ _, _, _, res_names, atom_names, idxs = zip(*group_iter)
212
+
213
+ # As of March 2023, all OTHER CHAINs in pdb are artificial nucleics.
214
+ is_nucleic_backbone = (
215
+ chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES
216
+ or chain_type == mmcif_names.OTHER_CHAIN
217
+ )
218
+ if chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES:
219
+ res_name = res_names[0]
220
+ if (
221
+ flatten_non_standard_residues
222
+ and res_name not in residue_names.PROTEIN_TYPES_WITH_UNKNOWN
223
+ and res_name != residue_names.MSE
224
+ ):
225
+ # For non-standard protein residues take all atoms.
226
+ # NOTE: This may get very large if we include hydrogens.
227
+ token_idxs.extend(idxs)
228
+ single_atom_token += [True] * len(idxs)
229
+ standard_token_idxs.extend([current_standard_token_id] * len(idxs))
230
+ else:
231
+ # For standard protein residues take 'CA' if it exists, else first atom.
232
+ if 'CA' in atom_names:
233
+ token_idxs.append(idxs[atom_names.index('CA')])
234
+ else:
235
+ token_idxs.append(idxs[0])
236
+ single_atom_token += [False]
237
+ standard_token_idxs.append(current_standard_token_id)
238
+ current_standard_token_id += 1
239
+ elif is_nucleic_backbone:
240
+ res_name = res_names[0]
241
+ if (
242
+ flatten_non_standard_residues
243
+ and res_name not in residue_names.NUCLEIC_TYPES_WITH_2_UNKS
244
+ ):
245
+ # For non-standard nucleic residues take all atoms.
246
+ token_idxs.extend(idxs)
247
+ single_atom_token += [True] * len(idxs)
248
+ standard_token_idxs.extend([current_standard_token_id] * len(idxs))
249
+ else:
250
+ # For standard nucleic residues take C1' if it exists, else first atom.
251
+ if "C1'" in atom_names:
252
+ token_idxs.append(idxs[atom_names.index("C1'")])
253
+ else:
254
+ token_idxs.append(idxs[0])
255
+ single_atom_token += [False]
256
+ standard_token_idxs.append(current_standard_token_id)
257
+ current_standard_token_id += 1
258
+ elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES:
259
+ # For non-polymers take all atoms
260
+ token_idxs.extend(idxs)
261
+ single_atom_token += [True] * len(idxs)
262
+ standard_token_idxs.extend([current_standard_token_id] * len(idxs))
263
+ current_standard_token_id += len(idxs)
264
+ else:
265
+ # Chain type that we don't handle yet.
266
+ logging.warning(
267
+ '%s: ignoring chain %s with chain type %s.',
268
+ logging_name,
269
+ chain_id,
270
+ chain_type,
271
+ )
272
+
273
+ assert len(token_idxs) == len(single_atom_token)
274
+ assert len(token_idxs) == len(standard_token_idxs)
275
+ standard_token_idxs = np.array(standard_token_idxs, dtype=np.int32)
276
+
277
+ # Create the list of all tokens, represented as a flat AtomLayout with 1
278
+ # representative atom per token.
279
+ all_tokens = flat_output_layout[token_idxs]
280
+
281
+ # Create the 2D atoms_per_token layout
282
+ num_tokens = all_tokens.shape[0]
283
+
284
+ # Target lists.
285
+ target_atom_names = []
286
+ target_atom_elements = []
287
+ target_res_ids = []
288
+ target_res_names = []
289
+ target_chain_ids = []
290
+ target_chain_types = []
291
+
292
+ # uids of all atoms in the flat layout, to check whether the dense atoms
293
+ # exist -- This is necessary for terminal atoms (e.g. 'OP3' or 'OXT')
294
+ all_atoms_uids = set(
295
+ zip(
296
+ flat_output_layout.chain_id,
297
+ flat_output_layout.res_id,
298
+ flat_output_layout.atom_name,
299
+ )
300
+ )
301
+
302
+ for idx, single_atom in enumerate(single_atom_token):
303
+ if not single_atom:
304
+ # Standard protein and nucleic residues have many atoms per token
305
+ chain_id = all_tokens.chain_id[idx]
306
+ res_id = all_tokens.res_id[idx]
307
+ res_name = all_tokens.res_name[idx]
308
+ atom_names = []
309
+ atom_elements = []
310
+
311
+ res_atoms = struc_chem_comps.get_all_atoms_in_entry(
312
+ ccd=ccd, res_name=res_name
313
+ )
314
+ atom_names_elements = list(
315
+ zip(
316
+ res_atoms['_chem_comp_atom.atom_id'],
317
+ res_atoms['_chem_comp_atom.type_symbol'],
318
+ strict=True,
319
+ )
320
+ )
321
+
322
+ for atom_name, atom_element in atom_names_elements:
323
+ # Remove hydrogens if they are not in flat layout.
324
+ if atom_element in ['H', 'D'] and (
325
+ (chain_id, res_id, atom_name) not in all_atoms_uids
326
+ ):
327
+ continue
328
+ elif (chain_id, res_id, atom_name) in all_atoms_uids:
329
+ atom_names.append(atom_name)
330
+ atom_elements.append(atom_element)
331
+ # Leave spaces for OXT etc.
332
+ else:
333
+ atom_names.append('')
334
+ atom_elements.append('')
335
+
336
+ if len(atom_names) > max_atoms_per_token:
337
+ logging.warning(
338
+ 'Atom list for chain %s '
339
+ 'residue %s %s is too long and will be truncated: '
340
+ '%s to the max atoms limit %s. Dropped atoms: %s',
341
+ chain_id,
342
+ res_id,
343
+ res_name,
344
+ len(atom_names),
345
+ max_atoms_per_token,
346
+ list(
347
+ zip(
348
+ atom_names[max_atoms_per_token:],
349
+ atom_elements[max_atoms_per_token:],
350
+ strict=True,
351
+ )
352
+ ),
353
+ )
354
+ atom_names = atom_names[:max_atoms_per_token]
355
+ atom_elements = atom_elements[:max_atoms_per_token]
356
+
357
+ num_pad = max_atoms_per_token - len(atom_names)
358
+ atom_names.extend([''] * num_pad)
359
+ atom_elements.extend([''] * num_pad)
360
+
361
+ else:
362
+ # ligands have only 1 atom per token
363
+ padding = [''] * (max_atoms_per_token - 1)
364
+ atom_names = [all_tokens.atom_name[idx]] + padding
365
+ atom_elements = [all_tokens.atom_element[idx]] + padding
366
+
367
+ # Append the atoms to the target lists.
368
+ target_atom_names.append(atom_names)
369
+ target_atom_elements.append(atom_elements)
370
+ target_res_names.append([all_tokens.res_name[idx]] * max_atoms_per_token)
371
+ target_res_ids.append([all_tokens.res_id[idx]] * max_atoms_per_token)
372
+ target_chain_ids.append([all_tokens.chain_id[idx]] * max_atoms_per_token)
373
+ target_chain_types.append(
374
+ [all_tokens.chain_type[idx]] * max_atoms_per_token
375
+ )
376
+
377
+ # Make sure to get the right shape also for 0 tokens
378
+ trg_shape = (num_tokens, max_atoms_per_token)
379
+ all_token_atoms_layout = atom_layout.AtomLayout(
380
+ atom_name=np.array(target_atom_names, dtype=object).reshape(trg_shape),
381
+ atom_element=np.array(target_atom_elements, dtype=object).reshape(
382
+ trg_shape
383
+ ),
384
+ res_name=np.array(target_res_names, dtype=object).reshape(trg_shape),
385
+ res_id=np.array(target_res_ids, dtype=int).reshape(trg_shape),
386
+ chain_id=np.array(target_chain_ids, dtype=object).reshape(trg_shape),
387
+ chain_type=np.array(target_chain_types, dtype=object).reshape(trg_shape),
388
+ )
389
+
390
+ return all_tokens, all_token_atoms_layout, standard_token_idxs
391
+
392
+
393
+ @dataclasses.dataclass(frozen=True)
394
+ class MSA:
395
+ """Dataclass containing MSA."""
396
+
397
+ rows: xnp_ndarray
398
+ mask: xnp_ndarray
399
+ deletion_matrix: xnp_ndarray
400
+ # Occurrence of each residue type along the sequence, averaged over MSA rows.
401
+ profile: xnp_ndarray
402
+ # Occurrence of deletions along the sequence, averaged over MSA rows.
403
+ deletion_mean: xnp_ndarray
404
+ # Number of MSA alignments.
405
+ num_alignments: xnp_ndarray
406
+
407
+ @classmethod
408
+ def compute_features(
409
+ cls,
410
+ *,
411
+ all_tokens: atom_layout.AtomLayout,
412
+ standard_token_idxs: np.ndarray,
413
+ padding_shapes: PaddingShapes,
414
+ fold_input: folding_input.Input,
415
+ logging_name: str,
416
+ max_paired_sequence_per_species: int,
417
+ resolve_msa_overlaps: bool = True,
418
+ ) -> Self:
419
+ """Compute the msa features."""
420
+ seen_entities = {}
421
+
422
+ substruct = atom_layout.make_structure(
423
+ flat_layout=all_tokens,
424
+ atom_coords=np.zeros(all_tokens.shape + (3,)),
425
+ name=logging_name,
426
+ )
427
+ prot = substruct.filter_to_entity_type(protein=True)
428
+ num_unique_chains = len(set(prot.chain_single_letter_sequence().values()))
429
+ need_msa_pairing = num_unique_chains > 1
430
+
431
+ np_chains_list = []
432
+ input_chains_by_id = {chain.id: chain for chain in fold_input.chains}
433
+ nonempty_chain_ids = set(all_tokens.chain_id)
434
+ for asym_id, chain_info in enumerate(substruct.iter_chains(), start=1):
435
+ b_chain_id = chain_info['chain_id']
436
+ chain_type = chain_info['chain_type']
437
+ chain = input_chains_by_id[b_chain_id]
438
+
439
+ # Generalised "sequence" for ligands (can't trust residue name)
440
+ chain_tokens = all_tokens[all_tokens.chain_id == b_chain_id]
441
+ assert chain_tokens.res_name is not None
442
+ three_letter_sequence = ','.join(chain_tokens.res_name.tolist())
443
+ chain_num_tokens = len(chain_tokens.atom_name)
444
+ if chain_type in mmcif_names.POLYMER_CHAIN_TYPES:
445
+ sequence = substruct.chain_single_letter_sequence()[b_chain_id]
446
+ if chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES:
447
+ # Only allow nucleic residue types for nucleic chains (can have some
448
+ # protein residues in e.g. tRNA, but that causes MSA search failures).
449
+ # Replace non nucleic residue types by UNK_NUCLEIC.
450
+ nucleic_types_one_letter = (
451
+ residue_names.DNA_TYPES_ONE_LETTER
452
+ + residue_names.RNA_TYPES_ONE_LETTER_WITH_UNKNOWN
453
+ )
454
+ sequence = ''.join([
455
+ base
456
+ if base in nucleic_types_one_letter
457
+ else residue_names.UNK_NUCLEIC_ONE_LETTER
458
+ for base in sequence
459
+ ])
460
+ else:
461
+ sequence = 'X' * chain_num_tokens
462
+
463
+ skip_chain = (
464
+ chain_type not in mmcif_names.STANDARD_POLYMER_CHAIN_TYPES
465
+ or len(sequence) <= 4
466
+ or b_chain_id not in nonempty_chain_ids
467
+ )
468
+ if three_letter_sequence in seen_entities:
469
+ entity_id = seen_entities[three_letter_sequence]
470
+ else:
471
+ entity_id = len(seen_entities) + 1
472
+
473
+ if chain_type in mmcif_names.STANDARD_POLYMER_CHAIN_TYPES:
474
+ unpaired_a3m = ''
475
+ paired_a3m = ''
476
+ if not skip_chain:
477
+ if need_msa_pairing and isinstance(chain, folding_input.ProteinChain):
478
+ paired_a3m = chain.paired_msa
479
+ if isinstance(
480
+ chain, folding_input.RnaChain | folding_input.ProteinChain
481
+ ):
482
+ unpaired_a3m = chain.unpaired_msa
483
+ # If we generated the MSA ourselves, it is already deduplicated. If it
484
+ # is user-provided, keep it as is to prevent destroying desired pairing.
485
+ unpaired_msa = msa_module.Msa.from_a3m(
486
+ query_sequence=sequence,
487
+ chain_poly_type=chain_type,
488
+ a3m=unpaired_a3m,
489
+ deduplicate=False,
490
+ )
491
+
492
+ paired_msa = msa_module.Msa.from_a3m(
493
+ query_sequence=sequence,
494
+ chain_poly_type=chain_type,
495
+ a3m=paired_a3m,
496
+ deduplicate=False,
497
+ )
498
+ else:
499
+ unpaired_msa = msa_module.Msa.from_empty(
500
+ query_sequence='-' * len(sequence),
501
+ chain_poly_type=mmcif_names.PROTEIN_CHAIN,
502
+ )
503
+ paired_msa = msa_module.Msa.from_empty(
504
+ query_sequence='-' * len(sequence),
505
+ chain_poly_type=mmcif_names.PROTEIN_CHAIN,
506
+ )
507
+
508
+ msa_features = unpaired_msa.featurize()
509
+ all_seqs_msa_features = paired_msa.featurize()
510
+
511
+ # msa_features = data3.fix_features(msa_features)
512
+ # all_seqs_msa_features = data3.fix_features(all_seqs_msa_features)
513
+
514
+ msa_features = msa_features | {
515
+ f'{k}_all_seq': v for k, v in all_seqs_msa_features.items()
516
+ }
517
+ feats = msa_features
518
+ feats['chain_id'] = b_chain_id
519
+ feats['asym_id'] = np.full(chain_num_tokens, asym_id)
520
+ feats['entity_id'] = entity_id
521
+ np_chains_list.append(feats)
522
+
523
+ # Add profile features to each chain.
524
+ for chain in np_chains_list:
525
+ chain.update(
526
+ data3.get_profile_features(chain['msa'], chain['deletion_matrix'])
527
+ )
528
+
529
+ # Allow 50% of the MSA to come from MSA pairing.
530
+ max_paired_sequences = padding_shapes.msa_size // 2
531
+ if need_msa_pairing:
532
+ np_chains_list = list(map(dict, np_chains_list))
533
+ np_chains_list = msa_pairing.create_paired_features(
534
+ np_chains_list,
535
+ max_paired_sequences=max_paired_sequences,
536
+ nonempty_chain_ids=nonempty_chain_ids,
537
+ max_hits_per_species=max_paired_sequence_per_species,
538
+ )
539
+ if resolve_msa_overlaps:
540
+ np_chains_list = msa_pairing.deduplicate_unpaired_sequences(
541
+ np_chains_list
542
+ )
543
+
544
+ # Remove all gapped rows from all seqs.
545
+ nonempty_asym_ids = []
546
+ for chain in np_chains_list:
547
+ if chain['chain_id'] in nonempty_chain_ids:
548
+ nonempty_asym_ids.append(chain['asym_id'][0])
549
+ if 'msa_all_seq' in np_chains_list[0]:
550
+ np_chains_list = msa_pairing.remove_all_gapped_rows_from_all_seqs(
551
+ np_chains_list, asym_ids=nonempty_asym_ids
552
+ )
553
+
554
+ # Crop MSA rows.
555
+ cropped_chains_list = []
556
+ for chain in np_chains_list:
557
+ unpaired_msa_size, paired_msa_size = (
558
+ msa_pairing.choose_paired_unpaired_msa_crop_sizes(
559
+ unpaired_msa=chain['msa'],
560
+ paired_msa=chain.get('msa_all_seq'),
561
+ total_msa_crop_size=padding_shapes.msa_size,
562
+ max_paired_sequences=max_paired_sequences,
563
+ )
564
+ )
565
+ cropped_chain = {
566
+ 'asym_id': chain['asym_id'],
567
+ 'chain_id': chain['chain_id'],
568
+ 'profile': chain['profile'],
569
+ 'deletion_mean': chain['deletion_mean'],
570
+ }
571
+ for feat in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES:
572
+ if feat in chain:
573
+ cropped_chain[feat] = chain[feat][:unpaired_msa_size]
574
+ if feat + '_all_seq' in chain:
575
+ cropped_chain[feat + '_all_seq'] = chain[feat + '_all_seq'][
576
+ :paired_msa_size
577
+ ]
578
+ cropped_chains_list.append(cropped_chain)
579
+
580
+ # Merge Chains.
581
+ # Make sure the chain order is unaltered before slicing with tokens.
582
+ curr_chain_order = [chain['chain_id'] for chain in cropped_chains_list]
583
+ orig_chain_order = [chain['chain_id'] for chain in substruct.iter_chains()]
584
+ assert curr_chain_order == orig_chain_order
585
+ np_example = {
586
+ 'asym_id': np.concatenate(
587
+ [c['asym_id'] for c in cropped_chains_list], axis=0
588
+ ),
589
+ }
590
+ for feature in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES:
591
+ for feat in [feature, feature + '_all_seq']:
592
+ if feat in cropped_chains_list[0]:
593
+ np_example[feat] = merging_features.merge_msa_features(
594
+ feat, cropped_chains_list
595
+ )
596
+ for feature in ['profile', 'deletion_mean']:
597
+ feature_list = [c[feature] for c in cropped_chains_list]
598
+ np_example[feature] = np.concatenate(feature_list, axis=0)
599
+
600
+ # Crop MSA rows to maximum size given by chains participating in the crop.
601
+ max_allowed_unpaired = max([
602
+ len(chain['msa'])
603
+ for chain in cropped_chains_list
604
+ if chain['asym_id'][0] in nonempty_asym_ids
605
+ ])
606
+ np_example['msa'] = np_example['msa'][:max_allowed_unpaired]
607
+ if 'msa_all_seq' in np_example:
608
+ max_allowed_paired = max([
609
+ len(chain['msa_all_seq'])
610
+ for chain in cropped_chains_list
611
+ if chain['asym_id'][0] in nonempty_asym_ids
612
+ ])
613
+ np_example['msa_all_seq'] = np_example['msa_all_seq'][:max_allowed_paired]
614
+
615
+ np_example = merging_features.merge_paired_and_unpaired_msa(np_example)
616
+
617
+ # Crop MSA residues. Need to use the standard token indices, since msa does
618
+ # not expand non-standard residues. This means that for expanded residues,
619
+ # we get repeated msa columns.
620
+ new_cropping_idxs = standard_token_idxs
621
+ for feature in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES:
622
+ if feature in np_example:
623
+ np_example[feature] = np_example[feature][:, new_cropping_idxs].copy()
624
+ for feature in ['profile', 'deletion_mean']:
625
+ np_example[feature] = np_example[feature][new_cropping_idxs]
626
+
627
+ # Make MSA mask.
628
+ np_example['msa_mask'] = np.ones_like(np_example['msa'], dtype=np.float32)
629
+
630
+ # Count MSA size before padding.
631
+ num_alignments = np_example['msa'].shape[0]
632
+
633
+ # Pad:
634
+ msa_size, num_tokens = padding_shapes.msa_size, padding_shapes.num_tokens
635
+
636
+ def safe_cast_int8(x):
637
+ return np.clip(x, np.iinfo(np.int8).min, np.iinfo(np.int8).max).astype(
638
+ np.int8
639
+ )
640
+
641
+ return MSA(
642
+ rows=_pad_to(safe_cast_int8(np_example['msa']), (msa_size, num_tokens)),
643
+ mask=_pad_to(
644
+ np_example['msa_mask'].astype(bool), (msa_size, num_tokens)
645
+ ),
646
+ # deletion_matrix may be out of int8 range, but we mostly care about
647
+ # small values since we arctan it in the model.
648
+ deletion_matrix=_pad_to(
649
+ safe_cast_int8(np_example['deletion_matrix']),
650
+ (msa_size, num_tokens),
651
+ ),
652
+ profile=_pad_to(np_example['profile'], (num_tokens, None)),
653
+ deletion_mean=_pad_to(np_example['deletion_mean'], (num_tokens,)),
654
+ num_alignments=np.array(num_alignments, dtype=np.int32),
655
+ )
656
+
657
+ def index_msa_rows(self, indices: xnp_ndarray) -> Self:
658
+ assert indices.ndim == 1
659
+
660
+ return MSA(
661
+ rows=self.rows[indices, :],
662
+ mask=self.mask[indices, :],
663
+ deletion_matrix=self.deletion_matrix[indices, :],
664
+ profile=self.profile,
665
+ deletion_mean=self.deletion_mean,
666
+ num_alignments=self.num_alignments,
667
+ )
668
+
669
+ @classmethod
670
+ def from_data_dict(cls, batch: BatchDict) -> Self:
671
+ output = cls(
672
+ rows=batch['msa'],
673
+ mask=batch['msa_mask'],
674
+ deletion_matrix=batch['deletion_matrix'],
675
+ profile=batch['profile'],
676
+ deletion_mean=batch['deletion_mean'],
677
+ num_alignments=batch['num_alignments'],
678
+ )
679
+ return output
680
+
681
+ def as_data_dict(self) -> BatchDict:
682
+ return {
683
+ 'msa': self.rows,
684
+ 'msa_mask': self.mask,
685
+ 'deletion_matrix': self.deletion_matrix,
686
+ 'profile': self.profile,
687
+ 'deletion_mean': self.deletion_mean,
688
+ 'num_alignments': self.num_alignments,
689
+ }
690
+
691
+
692
+ jax.tree_util.register_dataclass(
693
+ MSA,
694
+ data_fields=[f.name for f in dataclasses.fields(MSA)],
695
+ meta_fields=[],
696
+ )
697
+
698
+
699
+ @dataclasses.dataclass(frozen=True)
700
+ class Templates:
701
+ """Dataclass containing templates."""
702
+
703
+ # aatype of templates, int32 w shape [num_templates, num_res]
704
+ aatype: xnp_ndarray
705
+ # atom positions of templates, float32 w shape [num_templates, num_res, 24, 3]
706
+ atom_positions: xnp_ndarray
707
+ # atom mask of templates, bool w shape [num_templates, num_res, 24]
708
+ atom_mask: xnp_ndarray
709
+
710
+ @classmethod
711
+ def compute_features(
712
+ cls,
713
+ all_tokens: atom_layout.AtomLayout,
714
+ standard_token_idxs: np.ndarray,
715
+ padding_shapes: PaddingShapes,
716
+ fold_input: folding_input.Input,
717
+ max_templates: int,
718
+ logging_name: str,
719
+ ) -> Self:
720
+ """Compute the template features."""
721
+
722
+ seen_entities = {}
723
+ polymer_entity_features = {True: {}, False: {}}
724
+
725
+ substruct = atom_layout.make_structure(
726
+ flat_layout=all_tokens,
727
+ atom_coords=np.zeros(all_tokens.shape + (3,)),
728
+ name=logging_name,
729
+ )
730
+ np_chains_list = []
731
+
732
+ input_chains_by_id = {chain.id: chain for chain in fold_input.chains}
733
+
734
+ nonempty_chain_ids = set(all_tokens.chain_id)
735
+ for chain_info in substruct.iter_chains():
736
+ chain_id = chain_info['chain_id']
737
+ chain_type = chain_info['chain_type']
738
+ chain = input_chains_by_id[chain_id]
739
+
740
+ # Generalised "sequence" for ligands (can't trust residue name)
741
+ chain_tokens = all_tokens[all_tokens.chain_id == chain_id]
742
+ assert chain_tokens.res_name is not None
743
+ three_letter_sequence = ','.join(chain_tokens.res_name.tolist())
744
+ chain_num_tokens = len(chain_tokens.atom_name)
745
+
746
+ # Don't compute features for chains not included in the crop, or ligands.
747
+ skip_chain = (
748
+ chain_type != mmcif_names.PROTEIN_CHAIN
749
+ or chain_num_tokens <= 4 # not cache filled
750
+ or chain_id not in nonempty_chain_ids
751
+ )
752
+
753
+ if three_letter_sequence in seen_entities:
754
+ entity_id = seen_entities[three_letter_sequence]
755
+ else:
756
+ entity_id = len(seen_entities) + 1
757
+
758
+ if entity_id not in polymer_entity_features[skip_chain]:
759
+ if skip_chain:
760
+ template_features = data3.empty_template_features(chain_num_tokens)
761
+ else:
762
+ assert isinstance(chain, folding_input.ProteinChain)
763
+
764
+ sorted_features = []
765
+ for template in chain.templates:
766
+ struc = structure.from_mmcif(
767
+ template.mmcif,
768
+ fix_mse_residues=True,
769
+ fix_arginines=True,
770
+ include_bonds=False,
771
+ include_water=False,
772
+ include_other=True, # For non-standard polymer chains.
773
+ )
774
+ hit_features = templates.get_polymer_features(
775
+ chain=struc,
776
+ chain_poly_type=mmcif_names.PROTEIN_CHAIN,
777
+ query_sequence_length=len(chain.sequence),
778
+ query_to_hit_mapping=dict(template.query_to_template_map),
779
+ )
780
+ sorted_features.append(hit_features)
781
+
782
+ template_features = templates.package_template_features(
783
+ hit_features=sorted_features,
784
+ include_ligand_features=False,
785
+ )
786
+
787
+ template_features = data3.fix_template_features(
788
+ template_features=template_features, num_res=len(chain.sequence)
789
+ )
790
+
791
+ template_features = _reduce_template_features(
792
+ template_features, max_templates
793
+ )
794
+ polymer_entity_features[skip_chain][entity_id] = template_features
795
+
796
+ seen_entities[three_letter_sequence] = entity_id
797
+ feats = polymer_entity_features[skip_chain][entity_id].copy()
798
+ feats['chain_id'] = chain_id
799
+ np_chains_list.append(feats)
800
+
801
+ # We pad the num_templates dimension before merging, so that different
802
+ # chains can be concatenated on the num_res dimension. Masking will be
803
+ # applied so that each chains templates can't see each other.
804
+ for chain in np_chains_list:
805
+ chain['template_aatype'] = _pad_to(
806
+ chain['template_aatype'], (max_templates, None)
807
+ )
808
+ chain['template_atom_positions'] = _pad_to(
809
+ chain['template_atom_positions'], (max_templates, None, None, None)
810
+ )
811
+ chain['template_atom_mask'] = _pad_to(
812
+ chain['template_atom_mask'], (max_templates, None, None)
813
+ )
814
+
815
+ # Merge on token dimension.
816
+ np_example = {
817
+ ft: np.concatenate([c[ft] for c in np_chains_list], axis=1)
818
+ for ft in np_chains_list[0]
819
+ if ft in data_constants.TEMPLATE_FEATURES
820
+ }
821
+
822
+ # Crop template data. Need to use the standard token indices, since msa does
823
+ # not expand non-standard residues. This means that for expanded residues,
824
+ # we get repeated template information.
825
+ for feature_name, v in np_example.items():
826
+ np_example[feature_name] = v[:max_templates, standard_token_idxs, ...]
827
+
828
+ # Pad along the token dimension.
829
+ templates_features = Templates(
830
+ aatype=_pad_to(
831
+ np_example['template_aatype'], (None, padding_shapes.num_tokens)
832
+ ),
833
+ atom_positions=_pad_to(
834
+ np_example['template_atom_positions'],
835
+ (None, padding_shapes.num_tokens, None, None),
836
+ ),
837
+ atom_mask=_pad_to(
838
+ np_example['template_atom_mask'].astype(bool),
839
+ (None, padding_shapes.num_tokens, None),
840
+ ),
841
+ )
842
+ return templates_features
843
+
844
+ @classmethod
845
+ def from_data_dict(cls, batch: BatchDict) -> Self:
846
+ """Make Template from batch dictionary."""
847
+ return cls(
848
+ aatype=batch['template_aatype'],
849
+ atom_positions=batch['template_atom_positions'],
850
+ atom_mask=batch['template_atom_mask'],
851
+ )
852
+
853
+ def as_data_dict(self) -> BatchDict:
854
+ return {
855
+ 'template_aatype': self.aatype,
856
+ 'template_atom_positions': self.atom_positions,
857
+ 'template_atom_mask': self.atom_mask,
858
+ }
859
+
860
+
861
+ jax.tree_util.register_dataclass(
862
+ Templates,
863
+ data_fields=[f.name for f in dataclasses.fields(Templates)],
864
+ meta_fields=[],
865
+ )
866
+
867
+
868
+ def _reduce_template_features(
869
+ template_features: data3.FeatureDict,
870
+ max_templates: int,
871
+ ) -> data3.FeatureDict:
872
+ """Reduces template features to max num templates and defined feature set."""
873
+ num_templates = template_features['template_aatype'].shape[0]
874
+ template_keep_mask = np.arange(num_templates) < max_templates
875
+ template_fields = data_constants.TEMPLATE_FEATURES + (
876
+ 'template_release_timestamp',
877
+ )
878
+ template_features = {
879
+ k: v[template_keep_mask]
880
+ for k, v in template_features.items()
881
+ if k in template_fields
882
+ }
883
+ return template_features
884
+
885
+
886
+ @dataclasses.dataclass(frozen=True)
887
+ class TokenFeatures:
888
+ """Dataclass containing features for tokens."""
889
+
890
+ residue_index: xnp_ndarray
891
+ token_index: xnp_ndarray
892
+ aatype: xnp_ndarray
893
+ mask: xnp_ndarray
894
+ seq_length: xnp_ndarray
895
+
896
+ # Chain symmetry identifiers
897
+ # for an A3B2 stoichiometry the meaning of these features is as follows:
898
+ # asym_id: 1 2 3 4 5
899
+ # entity_id: 1 1 1 2 2
900
+ # sym_id: 1 2 3 1 2
901
+ asym_id: xnp_ndarray
902
+ entity_id: xnp_ndarray
903
+ sym_id: xnp_ndarray
904
+
905
+ # token type features
906
+ is_protein: xnp_ndarray
907
+ is_rna: xnp_ndarray
908
+ is_dna: xnp_ndarray
909
+ is_ligand: xnp_ndarray
910
+ is_nonstandard_polymer_chain: xnp_ndarray
911
+ is_water: xnp_ndarray
912
+
913
+ @classmethod
914
+ def compute_features(
915
+ cls,
916
+ all_tokens: atom_layout.AtomLayout,
917
+ padding_shapes: PaddingShapes,
918
+ ) -> Self:
919
+ """Compute the per-token features."""
920
+
921
+ residue_index = all_tokens.res_id.astype(np.int32)
922
+
923
+ token_index = np.arange(1, len(all_tokens.atom_name) + 1).astype(np.int32)
924
+
925
+ aatype = []
926
+ for res_name, chain_type in zip(all_tokens.res_name, all_tokens.chain_type):
927
+ if chain_type in mmcif_names.POLYMER_CHAIN_TYPES:
928
+ res_name = mmcif_names.fix_non_standard_polymer_res(
929
+ res_name=res_name, chain_type=chain_type
930
+ )
931
+ if (
932
+ chain_type == mmcif_names.DNA_CHAIN
933
+ and res_name == residue_names.UNK_DNA
934
+ ):
935
+ res_name = residue_names.UNK_NUCLEIC_ONE_LETTER
936
+ elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES:
937
+ res_name = residue_names.UNK
938
+ else:
939
+ raise ValueError(f'Chain type {chain_type} not polymer or ligand.')
940
+ aa = residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP[res_name]
941
+ aatype.append(aa)
942
+ aatype = np.array(aatype, dtype=np.int32)
943
+
944
+ mask = np.ones(all_tokens.shape[0], dtype=bool)
945
+ chains = _compute_asym_entity_and_sym_id(all_tokens)
946
+ m = dict(zip(chains.chain_id, chains.asym_id))
947
+ asym_id = np.array([m[c] for c in all_tokens.chain_id], dtype=np.int32)
948
+
949
+ m = dict(zip(chains.chain_id, chains.entity_id))
950
+ entity_id = np.array([m[c] for c in all_tokens.chain_id], dtype=np.int32)
951
+
952
+ m = dict(zip(chains.chain_id, chains.sym_id))
953
+ sym_id = np.array([m[c] for c in all_tokens.chain_id], dtype=np.int32)
954
+
955
+ seq_length = np.array(all_tokens.shape[0], dtype=np.int32)
956
+
957
+ is_protein = all_tokens.chain_type == mmcif_names.PROTEIN_CHAIN
958
+ is_rna = all_tokens.chain_type == mmcif_names.RNA_CHAIN
959
+ is_dna = all_tokens.chain_type == mmcif_names.DNA_CHAIN
960
+ is_ligand = np.isin(
961
+ all_tokens.chain_type, list(mmcif_names.LIGAND_CHAIN_TYPES)
962
+ )
963
+ standard_polymer_chain = list(mmcif_names.NON_POLYMER_CHAIN_TYPES) + list(
964
+ mmcif_names.STANDARD_POLYMER_CHAIN_TYPES
965
+ )
966
+ is_nonstandard_polymer_chain = np.isin(
967
+ all_tokens.chain_type, standard_polymer_chain, invert=True
968
+ )
969
+ is_water = all_tokens.chain_type == mmcif_names.WATER
970
+
971
+ return TokenFeatures(
972
+ residue_index=_pad_to(residue_index, (padding_shapes.num_tokens,)),
973
+ token_index=_pad_to(token_index, (padding_shapes.num_tokens,)),
974
+ aatype=_pad_to(aatype, (padding_shapes.num_tokens,)),
975
+ mask=_pad_to(mask, (padding_shapes.num_tokens,)),
976
+ asym_id=_pad_to(asym_id, (padding_shapes.num_tokens,)),
977
+ entity_id=_pad_to(entity_id, (padding_shapes.num_tokens,)),
978
+ sym_id=_pad_to(sym_id, (padding_shapes.num_tokens,)),
979
+ seq_length=seq_length,
980
+ is_protein=_pad_to(is_protein, (padding_shapes.num_tokens,)),
981
+ is_rna=_pad_to(is_rna, (padding_shapes.num_tokens,)),
982
+ is_dna=_pad_to(is_dna, (padding_shapes.num_tokens,)),
983
+ is_ligand=_pad_to(is_ligand, (padding_shapes.num_tokens,)),
984
+ is_nonstandard_polymer_chain=_pad_to(
985
+ is_nonstandard_polymer_chain, (padding_shapes.num_tokens,)
986
+ ),
987
+ is_water=_pad_to(is_water, (padding_shapes.num_tokens,)),
988
+ )
989
+
990
+ @classmethod
991
+ def from_data_dict(cls, batch: BatchDict) -> Self:
992
+ return cls(
993
+ residue_index=batch['residue_index'],
994
+ token_index=batch['token_index'],
995
+ aatype=batch['aatype'],
996
+ mask=batch['seq_mask'],
997
+ entity_id=batch['entity_id'],
998
+ asym_id=batch['asym_id'],
999
+ sym_id=batch['sym_id'],
1000
+ seq_length=batch['seq_length'],
1001
+ is_protein=batch['is_protein'],
1002
+ is_rna=batch['is_rna'],
1003
+ is_dna=batch['is_dna'],
1004
+ is_ligand=batch['is_ligand'],
1005
+ is_nonstandard_polymer_chain=batch['is_nonstandard_polymer_chain'],
1006
+ is_water=batch['is_water'],
1007
+ )
1008
+
1009
+ def as_data_dict(self) -> BatchDict:
1010
+ return {
1011
+ 'residue_index': self.residue_index,
1012
+ 'token_index': self.token_index,
1013
+ 'aatype': self.aatype,
1014
+ 'seq_mask': self.mask,
1015
+ 'entity_id': self.entity_id,
1016
+ 'asym_id': self.asym_id,
1017
+ 'sym_id': self.sym_id,
1018
+ 'seq_length': self.seq_length,
1019
+ 'is_protein': self.is_protein,
1020
+ 'is_rna': self.is_rna,
1021
+ 'is_dna': self.is_dna,
1022
+ 'is_ligand': self.is_ligand,
1023
+ 'is_nonstandard_polymer_chain': self.is_nonstandard_polymer_chain,
1024
+ 'is_water': self.is_water,
1025
+ }
1026
+
1027
+
1028
+ jax.tree_util.register_dataclass(
1029
+ TokenFeatures,
1030
+ data_fields=[f.name for f in dataclasses.fields(TokenFeatures)],
1031
+ meta_fields=[],
1032
+ )
1033
+
1034
+
1035
+ @dataclasses.dataclass(frozen=True)
1036
+ class PredictedStructureInfo:
1037
+ """Contains information necessary to work with predicted structure."""
1038
+
1039
+ atom_mask: xnp_ndarray
1040
+ residue_center_index: xnp_ndarray
1041
+
1042
+ @classmethod
1043
+ def compute_features(
1044
+ cls,
1045
+ all_tokens: atom_layout.AtomLayout,
1046
+ all_token_atoms_layout: atom_layout.AtomLayout,
1047
+ padding_shapes: PaddingShapes,
1048
+ ) -> Self:
1049
+ """Compute the PredictedStructureInfo features.
1050
+
1051
+ Args:
1052
+ all_tokens: flat AtomLayout with 1 representative atom per token, shape
1053
+ (num_tokens,)
1054
+ all_token_atoms_layout: AtomLayout for all atoms per token, shape
1055
+ (num_tokens, max_atoms_per_token)
1056
+ padding_shapes: padding shapes.
1057
+
1058
+ Returns:
1059
+ A PredictedStructureInfo object.
1060
+ """
1061
+ atom_mask = _pad_to(
1062
+ all_token_atoms_layout.atom_name.astype(bool),
1063
+ (padding_shapes.num_tokens, None),
1064
+ )
1065
+ residue_center_index = np.zeros(padding_shapes.num_tokens, dtype=np.int32)
1066
+ for idx in range(all_tokens.shape[0]):
1067
+ repr_atom = all_tokens.atom_name[idx]
1068
+ atoms = list(all_token_atoms_layout.atom_name[idx, :])
1069
+ if repr_atom in atoms:
1070
+ residue_center_index[idx] = atoms.index(repr_atom)
1071
+ else:
1072
+ # Representative atoms can be missing if cropping the number of atoms
1073
+ # per residue.
1074
+ logging.warning(
1075
+ 'The representative atom in all_tokens (%s) is not in '
1076
+ 'all_token_atoms_layout (%s)',
1077
+ all_tokens[idx : idx + 1],
1078
+ all_token_atoms_layout[idx, :],
1079
+ )
1080
+ residue_center_index[idx] = 0
1081
+ return cls(atom_mask=atom_mask, residue_center_index=residue_center_index)
1082
+
1083
+ @classmethod
1084
+ def from_data_dict(cls, batch: BatchDict) -> Self:
1085
+ return cls(
1086
+ atom_mask=batch['pred_dense_atom_mask'],
1087
+ residue_center_index=batch['residue_center_index'],
1088
+ )
1089
+
1090
+ def as_data_dict(self) -> BatchDict:
1091
+ return {
1092
+ 'pred_dense_atom_mask': self.atom_mask,
1093
+ 'residue_center_index': self.residue_center_index,
1094
+ }
1095
+
1096
+
1097
+ jax.tree_util.register_dataclass(
1098
+ PredictedStructureInfo,
1099
+ data_fields=[f.name for f in dataclasses.fields(PredictedStructureInfo)],
1100
+ meta_fields=[],
1101
+ )
1102
+
1103
+
1104
+ @dataclasses.dataclass(frozen=True)
1105
+ class PolymerLigandBondInfo:
1106
+ """Contains information about polymer-ligand bonds."""
1107
+
1108
+ tokens_to_polymer_ligand_bonds: atom_layout.GatherInfo
1109
+ # Gather indices to convert from cropped dense atom layout to bonds layout
1110
+ # (num_tokens, 2)
1111
+ token_atoms_to_bonds: atom_layout.GatherInfo
1112
+
1113
+ @classmethod
1114
+ def compute_features(
1115
+ cls,
1116
+ all_tokens: atom_layout.AtomLayout,
1117
+ all_token_atoms_layout: atom_layout.AtomLayout,
1118
+ bond_layout: atom_layout.AtomLayout | None,
1119
+ padding_shapes: PaddingShapes,
1120
+ ) -> Self:
1121
+ """Computes the InterChainBondInfo features.
1122
+
1123
+ Args:
1124
+ all_tokens: AtomLayout for tokens; shape (num_tokens,).
1125
+ all_token_atoms_layout: Atom Layout for all atoms (num_tokens,
1126
+ max_atoms_per_token)
1127
+ bond_layout: Bond layout for polymer-ligand bonds.
1128
+ padding_shapes: Padding shapes.
1129
+
1130
+ Returns:
1131
+ A PolymerLigandBondInfo object.
1132
+ """
1133
+
1134
+ if bond_layout is not None:
1135
+ # Must convert to list before calling np.isin, will not work raw.
1136
+ peptide_types = list(mmcif_names.PEPTIDE_CHAIN_TYPES)
1137
+ nucleic_types = list(mmcif_names.NUCLEIC_ACID_CHAIN_TYPES) + [
1138
+ mmcif_names.OTHER_CHAIN
1139
+ ]
1140
+ # These atom renames are so that we can use the atom layout code with
1141
+ # all_tokens, which only has a single atom per token.
1142
+ atom_names = bond_layout.atom_name.copy()
1143
+ atom_names[np.isin(bond_layout.chain_type, peptide_types)] = 'CA'
1144
+ atom_names[np.isin(bond_layout.chain_type, nucleic_types)] = "C1'"
1145
+ adjusted_bond_layout = atom_layout.AtomLayout(
1146
+ atom_name=atom_names,
1147
+ res_id=bond_layout.res_id,
1148
+ chain_id=bond_layout.chain_id,
1149
+ chain_type=bond_layout.chain_type,
1150
+ )
1151
+ # Remove bonds that are not in the crop.
1152
+ cropped_tokens_to_bonds = atom_layout.compute_gather_idxs(
1153
+ source_layout=all_tokens, target_layout=adjusted_bond_layout
1154
+ )
1155
+ bond_is_in_crop = np.all(
1156
+ cropped_tokens_to_bonds.gather_mask, axis=1
1157
+ ).astype(bool)
1158
+ adjusted_bond_layout = adjusted_bond_layout[bond_is_in_crop, :]
1159
+ else:
1160
+ # Create layout with correct shape when bond_layout is None.
1161
+ s = (0, 2)
1162
+ adjusted_bond_layout = atom_layout.AtomLayout(
1163
+ atom_name=np.array([], dtype=object).reshape(s),
1164
+ res_id=np.array([], dtype=int).reshape(s),
1165
+ chain_id=np.array([], dtype=object).reshape(s),
1166
+ )
1167
+ adjusted_bond_layout = adjusted_bond_layout.copy_and_pad_to(
1168
+ (padding_shapes.num_tokens, 2)
1169
+ )
1170
+ tokens_to_polymer_ligand_bonds = atom_layout.compute_gather_idxs(
1171
+ source_layout=all_tokens, target_layout=adjusted_bond_layout
1172
+ )
1173
+
1174
+ # Stuff for computing the bond loss.
1175
+ if bond_layout is not None:
1176
+ # Pad to num_tokens (hoping that there are never more bonds than tokens).
1177
+ padded_bond_layout = bond_layout.copy_and_pad_to(
1178
+ (padding_shapes.num_tokens, 2)
1179
+ )
1180
+ token_atoms_to_bonds = atom_layout.compute_gather_idxs(
1181
+ source_layout=all_token_atoms_layout, target_layout=padded_bond_layout
1182
+ )
1183
+ else:
1184
+ token_atoms_to_bonds = atom_layout.GatherInfo(
1185
+ gather_idxs=np.zeros((padding_shapes.num_tokens, 2), dtype=int),
1186
+ gather_mask=np.zeros((padding_shapes.num_tokens, 2), dtype=bool),
1187
+ input_shape=np.array((
1188
+ padding_shapes.num_tokens,
1189
+ all_token_atoms_layout.shape[1],
1190
+ )),
1191
+ )
1192
+
1193
+ return cls(
1194
+ tokens_to_polymer_ligand_bonds=tokens_to_polymer_ligand_bonds,
1195
+ token_atoms_to_bonds=token_atoms_to_bonds,
1196
+ )
1197
+
1198
+ @classmethod
1199
+ def from_data_dict(cls, batch: BatchDict) -> Self:
1200
+ return cls(
1201
+ tokens_to_polymer_ligand_bonds=atom_layout.GatherInfo.from_dict(
1202
+ batch, key_prefix='tokens_to_polymer_ligand_bonds'
1203
+ ),
1204
+ token_atoms_to_bonds=atom_layout.GatherInfo.from_dict(
1205
+ batch, key_prefix='token_atoms_to_polymer_ligand_bonds'
1206
+ ),
1207
+ )
1208
+
1209
+ def as_data_dict(self) -> BatchDict:
1210
+ return {
1211
+ **self.tokens_to_polymer_ligand_bonds.as_dict(
1212
+ key_prefix='tokens_to_polymer_ligand_bonds'
1213
+ ),
1214
+ **self.token_atoms_to_bonds.as_dict(
1215
+ key_prefix='token_atoms_to_polymer_ligand_bonds'
1216
+ ),
1217
+ }
1218
+
1219
+
1220
+ jax.tree_util.register_dataclass(
1221
+ PolymerLigandBondInfo,
1222
+ data_fields=[f.name for f in dataclasses.fields(PolymerLigandBondInfo)],
1223
+ meta_fields=[],
1224
+ )
1225
+
1226
+
1227
+ @dataclasses.dataclass(frozen=True)
1228
+ class LigandLigandBondInfo:
1229
+ """Contains information about the location of ligand-ligand bonds."""
1230
+
1231
+ tokens_to_ligand_ligand_bonds: atom_layout.GatherInfo
1232
+
1233
+ @classmethod
1234
+ def compute_features(
1235
+ cls,
1236
+ all_tokens: atom_layout.AtomLayout,
1237
+ bond_layout: atom_layout.AtomLayout | None,
1238
+ padding_shapes: PaddingShapes,
1239
+ ) -> Self:
1240
+ """Computes the InterChainBondInfo features.
1241
+
1242
+ Args:
1243
+ all_tokens: AtomLayout for tokens; shape (num_tokens,).
1244
+ bond_layout: Bond layout for ligand-ligand bonds.
1245
+ padding_shapes: Padding shapes.
1246
+
1247
+ Returns:
1248
+ A LigandLigandBondInfo object.
1249
+ """
1250
+
1251
+ if bond_layout is not None:
1252
+ # Discard any bonds that do not join to an existing atom.
1253
+ keep_mask = []
1254
+ all_atom_ids = {
1255
+ uid
1256
+ for uid in zip(
1257
+ all_tokens.chain_id,
1258
+ all_tokens.res_id,
1259
+ all_tokens.atom_name,
1260
+ strict=True,
1261
+ )
1262
+ }
1263
+ for chain_id, res_id, atom_name in zip(
1264
+ bond_layout.chain_id,
1265
+ bond_layout.res_id,
1266
+ bond_layout.atom_name,
1267
+ strict=True,
1268
+ ):
1269
+ atom_a = (chain_id[0], res_id[0], atom_name[0])
1270
+ atom_b = (chain_id[1], res_id[1], atom_name[1])
1271
+ if atom_a in all_atom_ids and atom_b in all_atom_ids:
1272
+ keep_mask.append(True)
1273
+ else:
1274
+ keep_mask.append(False)
1275
+ keep_mask = np.array(keep_mask).astype(bool)
1276
+ bond_layout = bond_layout[keep_mask]
1277
+ # Remove any bonds to Hydrogen atoms.
1278
+ bond_layout = bond_layout[
1279
+ ~np.char.startswith(bond_layout.atom_name.astype(str), 'H').any(
1280
+ axis=1
1281
+ )
1282
+ ]
1283
+ atom_names = bond_layout.atom_name
1284
+ adjusted_bond_layout = atom_layout.AtomLayout(
1285
+ atom_name=atom_names,
1286
+ res_id=bond_layout.res_id,
1287
+ chain_id=bond_layout.chain_id,
1288
+ chain_type=bond_layout.chain_type,
1289
+ )
1290
+ else:
1291
+ # Create layout with correct shape when bond_layout is None.
1292
+ s = (0, 2)
1293
+ adjusted_bond_layout = atom_layout.AtomLayout(
1294
+ atom_name=np.array([], dtype=object).reshape(s),
1295
+ res_id=np.array([], dtype=int).reshape(s),
1296
+ chain_id=np.array([], dtype=object).reshape(s),
1297
+ )
1298
+ # 10 x num_tokens as max_inter_bonds_ratio + max_intra_bonds_ration = 2.061.
1299
+ adjusted_bond_layout = adjusted_bond_layout.copy_and_pad_to(
1300
+ (padding_shapes.num_tokens * 10, 2)
1301
+ )
1302
+ gather_idx = atom_layout.compute_gather_idxs(
1303
+ source_layout=all_tokens, target_layout=adjusted_bond_layout
1304
+ )
1305
+ return cls(tokens_to_ligand_ligand_bonds=gather_idx)
1306
+
1307
+ @classmethod
1308
+ def from_data_dict(cls, batch: BatchDict) -> Self:
1309
+ return cls(
1310
+ tokens_to_ligand_ligand_bonds=atom_layout.GatherInfo.from_dict(
1311
+ batch, key_prefix='tokens_to_ligand_ligand_bonds'
1312
+ )
1313
+ )
1314
+
1315
+ def as_data_dict(self) -> BatchDict:
1316
+ return {
1317
+ **self.tokens_to_ligand_ligand_bonds.as_dict(
1318
+ key_prefix='tokens_to_ligand_ligand_bonds'
1319
+ )
1320
+ }
1321
+
1322
+
1323
+ jax.tree_util.register_dataclass(
1324
+ LigandLigandBondInfo,
1325
+ data_fields=[f.name for f in dataclasses.fields(LigandLigandBondInfo)],
1326
+ meta_fields=[],
1327
+ )
1328
+
1329
+
1330
+ @dataclasses.dataclass(frozen=True)
1331
+ class PseudoBetaInfo:
1332
+ """Contains information for extracting pseudo-beta and equivalent atoms."""
1333
+
1334
+ token_atoms_to_pseudo_beta: atom_layout.GatherInfo
1335
+
1336
+ @classmethod
1337
+ def compute_features(
1338
+ cls,
1339
+ all_token_atoms_layout: atom_layout.AtomLayout,
1340
+ ccd: chemical_components.Ccd,
1341
+ padding_shapes: PaddingShapes,
1342
+ logging_name: str,
1343
+ ) -> Self:
1344
+ """Compute the PseudoBetaInfo features.
1345
+
1346
+ Args:
1347
+ all_token_atoms_layout: AtomLayout for all atoms per token, shape
1348
+ (num_tokens, max_atoms_per_token)
1349
+ ccd: The chemical components dictionary.
1350
+ padding_shapes: padding shapes.
1351
+ logging_name: logging name for debugging (usually the mmcif_id)
1352
+
1353
+ Returns:
1354
+ A PseudoBetaInfo object.
1355
+ """
1356
+ token_idxs = []
1357
+ atom_idxs = []
1358
+ for token_idx in range(all_token_atoms_layout.shape[0]):
1359
+ chain_type = all_token_atoms_layout.chain_type[token_idx, 0]
1360
+ atom_names = list(all_token_atoms_layout.atom_name[token_idx, :])
1361
+ atom_idx = None
1362
+ is_nucleic_backbone = (
1363
+ chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES
1364
+ or chain_type == mmcif_names.OTHER_CHAIN
1365
+ )
1366
+ if chain_type == mmcif_names.PROTEIN_CHAIN:
1367
+ # Protein chains
1368
+ if 'CB' in atom_names:
1369
+ atom_idx = atom_names.index('CB')
1370
+ elif 'CA' in atom_names:
1371
+ atom_idx = atom_names.index('CA')
1372
+ elif is_nucleic_backbone:
1373
+ # RNA / DNA chains
1374
+ res_name = all_token_atoms_layout.res_name[token_idx, 0]
1375
+ cifdict = ccd.get(res_name)
1376
+ if cifdict:
1377
+ parent = cifdict['_chem_comp.mon_nstd_parent_comp_id'][0]
1378
+ if parent != '?':
1379
+ res_name = parent
1380
+ if res_name in {'A', 'G', 'DA', 'DG'}:
1381
+ if 'C4' in atom_names:
1382
+ atom_idx = atom_names.index('C4')
1383
+ else:
1384
+ if 'C2' in atom_names:
1385
+ atom_idx = atom_names.index('C2')
1386
+ elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES:
1387
+ # Ligands: there is only one atom per token
1388
+ atom_idx = 0
1389
+ else:
1390
+ logging.warning(
1391
+ '%s: Unknown chain type for token %i. (%s)',
1392
+ logging_name,
1393
+ token_idx,
1394
+ all_token_atoms_layout[token_idx : token_idx + 1],
1395
+ )
1396
+ atom_idx = 0
1397
+ if atom_idx is None:
1398
+ (valid_atom_idxs,) = np.nonzero(
1399
+ all_token_atoms_layout.atom_name[token_idx, :]
1400
+ )
1401
+ if valid_atom_idxs.shape[0] > 0:
1402
+ atom_idx = valid_atom_idxs[0]
1403
+ else:
1404
+ atom_idx = 0
1405
+ logging.warning(
1406
+ '%s token %i (%s), does not contain a pseudo-beta atom.'
1407
+ 'Using first valid atom (%s) instead.',
1408
+ logging_name,
1409
+ token_idx,
1410
+ all_token_atoms_layout[token_idx : token_idx + 1],
1411
+ all_token_atoms_layout.atom_name[token_idx, atom_idx],
1412
+ )
1413
+
1414
+ token_idxs.append(token_idx)
1415
+ atom_idxs.append(atom_idx)
1416
+
1417
+ pseudo_beta_layout = all_token_atoms_layout[token_idxs, atom_idxs]
1418
+ pseudo_beta_layout = pseudo_beta_layout.copy_and_pad_to((
1419
+ padding_shapes.num_tokens,
1420
+ ))
1421
+ token_atoms_to_pseudo_beta = atom_layout.compute_gather_idxs(
1422
+ source_layout=all_token_atoms_layout, target_layout=pseudo_beta_layout
1423
+ )
1424
+
1425
+ return cls(
1426
+ token_atoms_to_pseudo_beta=token_atoms_to_pseudo_beta,
1427
+ )
1428
+
1429
+ @classmethod
1430
+ def from_data_dict(cls, batch: BatchDict) -> Self:
1431
+ return cls(
1432
+ token_atoms_to_pseudo_beta=atom_layout.GatherInfo.from_dict(
1433
+ batch, key_prefix='token_atoms_to_pseudo_beta'
1434
+ ),
1435
+ )
1436
+
1437
+ def as_data_dict(self) -> BatchDict:
1438
+ return {
1439
+ **self.token_atoms_to_pseudo_beta.as_dict(
1440
+ key_prefix='token_atoms_to_pseudo_beta'
1441
+ ),
1442
+ }
1443
+
1444
+
1445
+ jax.tree_util.register_dataclass(
1446
+ PseudoBetaInfo,
1447
+ data_fields=[f.name for f in dataclasses.fields(PseudoBetaInfo)],
1448
+ meta_fields=[],
1449
+ )
1450
+
1451
+
1452
+ _DEFAULT_BLANK_REF = {
1453
+ 'positions': np.zeros(3),
1454
+ 'mask': 0,
1455
+ 'element': 0,
1456
+ 'charge': 0,
1457
+ 'atom_name_chars': np.zeros(4),
1458
+ }
1459
+
1460
+
1461
+ def random_rotation(random_state: np.random.RandomState) -> np.ndarray:
1462
+ # Create a random rotation (Gram-Schmidt orthogonalization of two
1463
+ # random normal vectors)
1464
+ v0, v1 = random_state.normal(size=(2, 3))
1465
+ e0 = v0 / np.maximum(1e-10, np.linalg.norm(v0))
1466
+ v1 = v1 - e0 * np.dot(v1, e0)
1467
+ e1 = v1 / np.maximum(1e-10, np.linalg.norm(v1))
1468
+ e2 = np.cross(e0, e1)
1469
+ return np.stack([e0, e1, e2])
1470
+
1471
+
1472
+ def random_augmentation(
1473
+ positions: np.ndarray,
1474
+ random_state: np.random.RandomState,
1475
+ ) -> np.ndarray:
1476
+ """Center then apply random translation and rotation."""
1477
+
1478
+ center = np.mean(positions, axis=0)
1479
+ rot = random_rotation(random_state)
1480
+ positions_target = np.einsum('ij,kj->ki', rot, positions - center)
1481
+
1482
+ translation = random_state.normal(size=(3,))
1483
+ positions_target = positions_target + translation
1484
+ return positions_target
1485
+
1486
+
1487
+ def _get_reference_positions_from_ccd_cif(
1488
+ ccd_cif: cif_dict.CifDict,
1489
+ ref_max_modified_date: datetime.date,
1490
+ logging_name: str,
1491
+ ) -> np.ndarray:
1492
+ """Creates reference positions from a CCD mmcif data block."""
1493
+ num_atoms = len(ccd_cif['_chem_comp_atom.atom_id'])
1494
+ if '_chem_comp_atom.pdbx_model_Cartn_x_ideal' in ccd_cif:
1495
+ atom_x = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_x_ideal']
1496
+ atom_y = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_y_ideal']
1497
+ atom_z = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_z_ideal']
1498
+ else:
1499
+ atom_x = np.array(['?'] * num_atoms)
1500
+ atom_y = np.array(['?'] * num_atoms)
1501
+ atom_z = np.array(['?'] * num_atoms)
1502
+ pos = np.array([[x, y, z] for x, y, z in zip(atom_x, atom_y, atom_z)])
1503
+ # Unknown reference coordinates are specified by '?' in chem comp dict.
1504
+ # Replace unknown reference coords with 0.
1505
+ if '?' in pos and '_chem_comp.pdbx_modified_date' in ccd_cif:
1506
+ # Use reference coordinates if modifed date is before cutoff.
1507
+ modified_dates = [
1508
+ datetime.date.fromisoformat(date)
1509
+ for date in ccd_cif['_chem_comp.pdbx_modified_date']
1510
+ ]
1511
+ max_modified_date = max(modified_dates)
1512
+ if max_modified_date < ref_max_modified_date:
1513
+ atom_x = ccd_cif['_chem_comp_atom.model_Cartn_x']
1514
+ atom_y = ccd_cif['_chem_comp_atom.model_Cartn_y']
1515
+ atom_z = ccd_cif['_chem_comp_atom.model_Cartn_z']
1516
+ pos = np.array([[x, y, z] for x, y, z in zip(atom_x, atom_y, atom_z)])
1517
+ if '?' in pos:
1518
+ if np.all(pos == '?'):
1519
+ logging.warning('All ref positions unknown for: %s', logging_name)
1520
+ else:
1521
+ logging.warning('Some ref positions unknown for: %s', logging_name)
1522
+ pos[pos == '?'] = 0
1523
+ return np.array(pos, dtype=np.float32)
1524
+
1525
+
1526
+ def get_reference(
1527
+ res_name: str,
1528
+ chemical_components_data: struc_chem_comps.ChemicalComponentsData,
1529
+ ccd: chemical_components.Ccd,
1530
+ random_state: np.random.RandomState,
1531
+ ref_max_modified_date: datetime.date,
1532
+ conformer_max_iterations: int | None,
1533
+ ) -> tuple[dict[str, Any], Any, Any]:
1534
+ """Reference structure for residue from CCD or SMILES.
1535
+
1536
+ Uses CCD entry if available, otherwise uses SMILES from chemical components
1537
+ data. Conformer generation is done using RDKit, with a fallback to CCD ideal
1538
+ or reference coordinates if RDKit fails and those coordinates are supplied.
1539
+
1540
+ Args:
1541
+ res_name: ccd code of the residue.
1542
+ chemical_components_data: ChemicalComponentsData for making ref structure.
1543
+ ccd: The chemical components dictionary.
1544
+ random_state: Numpy RandomState
1545
+ ref_max_modified_date: date beyond which reference structures must not be
1546
+ modified to be allowed to use reference coordinates.
1547
+ conformer_max_iterations: Optional override for maximum number of iterations
1548
+ to run for RDKit conformer search.
1549
+
1550
+ Returns:
1551
+ Mapping from atom names to features, from_atoms, dest_atoms.
1552
+ """
1553
+
1554
+ ccd_cif = ccd.get(res_name)
1555
+
1556
+ mol = None
1557
+ if ccd_cif:
1558
+ try:
1559
+ mol = rdkit_utils.mol_from_ccd_cif(ccd_cif, remove_hydrogens=False)
1560
+ except rdkit_utils.MolFromMmcifError:
1561
+ logging.warning('Failed to construct mol from ccd_cif for: %s', res_name)
1562
+ else: # No CCD entry, use SMILES from chemical components data.
1563
+ if not (
1564
+ chemical_components_data.chem_comp
1565
+ and res_name in chemical_components_data.chem_comp
1566
+ and chemical_components_data.chem_comp[res_name].pdbx_smiles
1567
+ ):
1568
+ raise ValueError(f'No CCD entry or SMILES for {res_name}.')
1569
+ smiles_string = chemical_components_data.chem_comp[res_name].pdbx_smiles
1570
+ logging.info('Using SMILES for: %s - %s', res_name, smiles_string)
1571
+
1572
+ mol = Chem.MolFromSmiles(smiles_string)
1573
+ if mol is None:
1574
+ # In this case the model will not have any information about this molecule
1575
+ # and will not be able to predict anything about it.
1576
+ raise ValueError(
1577
+ f'Failed to construct RDKit Mol for {res_name} from SMILES string: '
1578
+ f'{smiles_string} . This is likely due to an issue with the SMILES '
1579
+ 'string. Note that the userCCD input format provides an alternative '
1580
+ 'way to define custom molecules directly without RDKit or SMILES.'
1581
+ )
1582
+ mol = Chem.AddHs(mol)
1583
+ # No existing names, we assign them from the graph.
1584
+ mol = rdkit_utils.assign_atom_names_from_graph(mol)
1585
+ # Temporary CCD cif with just atom and bond information, no coordinates.
1586
+ ccd_cif = rdkit_utils.mol_to_ccd_cif(mol, component_id='fake_cif')
1587
+
1588
+ conformer = None
1589
+ atom_names = []
1590
+ elements = []
1591
+ charges = []
1592
+ pos = []
1593
+
1594
+ # If mol is not None (must be True for SMILES case), then we try and generate
1595
+ # an RDKit conformer.
1596
+ if mol is not None:
1597
+ conformer_random_seed = int(random_state.randint(1, 1 << 31))
1598
+ conformer = rdkit_utils.get_random_conformer(
1599
+ mol=mol,
1600
+ random_seed=conformer_random_seed,
1601
+ max_iterations=conformer_max_iterations,
1602
+ logging_name=res_name,
1603
+ )
1604
+ if conformer:
1605
+ for idx, atom in enumerate(mol.GetAtoms()):
1606
+ atom_names.append(atom.GetProp('atom_name'))
1607
+ elements.append(atom.GetAtomicNum())
1608
+ charges.append(atom.GetFormalCharge())
1609
+ coords = conformer.GetAtomPosition(idx)
1610
+ pos.append([coords.x, coords.y, coords.z])
1611
+ pos = np.array(pos, dtype=np.float32)
1612
+
1613
+ # If no mol could be generated (can only happen when using CCD), or no
1614
+ # conformer could be generated from the mol (can happen in either case), then
1615
+ # use CCD cif instead (which will have zero coordinates for SMILES case).
1616
+ if conformer is None:
1617
+ atom_names = ccd_cif['_chem_comp_atom.atom_id']
1618
+ charges = ccd_cif['_chem_comp_atom.charge']
1619
+ type_symbols = ccd_cif['_chem_comp_atom.type_symbol']
1620
+ elements = [
1621
+ periodic_table.ATOMIC_NUMBER.get(elem_type.capitalize(), 0)
1622
+ for elem_type in type_symbols
1623
+ ]
1624
+ pos = _get_reference_positions_from_ccd_cif(
1625
+ ccd_cif=ccd_cif,
1626
+ ref_max_modified_date=ref_max_modified_date,
1627
+ logging_name=res_name,
1628
+ )
1629
+
1630
+ # Augment reference positions.
1631
+ pos = random_augmentation(pos, random_state)
1632
+
1633
+ # Extract atom and bond information from CCD cif.
1634
+ from_atom = ccd_cif.get('_chem_comp_bond.atom_id_1', None)
1635
+ dest_atom = ccd_cif.get('_chem_comp_bond.atom_id_2', None)
1636
+
1637
+ features = {}
1638
+ for atom_name in atom_names:
1639
+ features[atom_name] = {}
1640
+ idx = atom_names.index(atom_name)
1641
+ charge = 0 if charges[idx] == '?' else int(charges[idx])
1642
+ atom_name_chars = np.array([ord(c) - 32 for c in atom_name], dtype=int)
1643
+ atom_name_chars = _pad_to(atom_name_chars, (4,))
1644
+ features[atom_name]['positions'] = pos[idx]
1645
+ features[atom_name]['mask'] = 1
1646
+ features[atom_name]['element'] = elements[idx]
1647
+ features[atom_name]['charge'] = charge
1648
+ features[atom_name]['atom_name_chars'] = atom_name_chars
1649
+ return features, from_atom, dest_atom
1650
+
1651
+
1652
+ @dataclasses.dataclass(frozen=True)
1653
+ class RefStructure:
1654
+ """Contains ref structure information."""
1655
+
1656
+ # Array with positions, float32, shape [num_res, max_atoms_per_token, 3]
1657
+ positions: xnp_ndarray
1658
+ # Array with masks, bool, shape [num_res, max_atoms_per_token]
1659
+ mask: xnp_ndarray
1660
+ # Array with elements, int32, shape [num_res, max_atoms_per_token]
1661
+ element: xnp_ndarray
1662
+ # Array with charges, float32, shape [num_res, max_atoms_per_token]
1663
+ charge: xnp_ndarray
1664
+ # Array with atom name characters, int32, [num_res, max_atoms_per_token, 4]
1665
+ atom_name_chars: xnp_ndarray
1666
+ # Array with reference space uids, int32, [num_res, max_atoms_per_token]
1667
+ ref_space_uid: xnp_ndarray
1668
+
1669
+ @classmethod
1670
+ def compute_features(
1671
+ cls,
1672
+ all_token_atoms_layout: atom_layout.AtomLayout,
1673
+ ccd: chemical_components.Ccd,
1674
+ padding_shapes: PaddingShapes,
1675
+ chemical_components_data: struc_chem_comps.ChemicalComponentsData,
1676
+ random_state: np.random.RandomState,
1677
+ ref_max_modified_date: datetime.date,
1678
+ conformer_max_iterations: int | None,
1679
+ ligand_ligand_bonds: atom_layout.AtomLayout | None = None,
1680
+ ) -> tuple[Self, Any]:
1681
+ """Reference structure information for each residue."""
1682
+
1683
+ # Get features per atom
1684
+ padded_shape = (padding_shapes.num_tokens, all_token_atoms_layout.shape[1])
1685
+ result = {
1686
+ 'positions': np.zeros((*padded_shape, 3), 'float32'),
1687
+ 'mask': np.zeros(padded_shape, 'bool'),
1688
+ 'element': np.zeros(padded_shape, 'int32'),
1689
+ 'charge': np.zeros(padded_shape, 'float32'),
1690
+ 'atom_name_chars': np.zeros((*padded_shape, 4), 'int32'),
1691
+ 'ref_space_uid': np.zeros((*padded_shape,), 'int32'),
1692
+ }
1693
+
1694
+ atom_names_all = []
1695
+ chain_ids_all = []
1696
+ res_ids_all = []
1697
+
1698
+ # Cache reference conformations for each residue.
1699
+ conformations = {}
1700
+ ref_space_uids = {}
1701
+ for idx in np.ndindex(all_token_atoms_layout.shape):
1702
+ chain_id = all_token_atoms_layout.chain_id[idx]
1703
+ res_id = all_token_atoms_layout.res_id[idx]
1704
+ res_name = all_token_atoms_layout.res_name[idx]
1705
+ is_non_standard = res_name not in _STANDARD_RESIDUES
1706
+ atom_name = all_token_atoms_layout.atom_name[idx]
1707
+ if not atom_name:
1708
+ ref = _DEFAULT_BLANK_REF
1709
+ else:
1710
+ if (chain_id, res_id) not in conformations:
1711
+ conf, from_atom, dest_atom = get_reference(
1712
+ res_name=res_name,
1713
+ chemical_components_data=chemical_components_data,
1714
+ ccd=ccd,
1715
+ random_state=random_state,
1716
+ ref_max_modified_date=ref_max_modified_date,
1717
+ conformer_max_iterations=conformer_max_iterations,
1718
+ )
1719
+ conformations[(chain_id, res_id)] = conf
1720
+
1721
+ if (
1722
+ is_non_standard
1723
+ and (from_atom is not None)
1724
+ and (dest_atom is not None)
1725
+ ):
1726
+ # Add intra-ligand bond graph
1727
+ atom_names_ligand = np.stack(
1728
+ [from_atom, dest_atom], axis=1, dtype=object
1729
+ )
1730
+ atom_names_all.append(atom_names_ligand)
1731
+ res_ids_all.append(
1732
+ np.full_like(atom_names_ligand, res_id, dtype=int)
1733
+ )
1734
+ chain_ids_all.append(
1735
+ np.full_like(atom_names_ligand, chain_id, dtype=object)
1736
+ )
1737
+
1738
+ conformation = conformations.get(
1739
+ (chain_id, res_id), {atom_name: _DEFAULT_BLANK_REF}
1740
+ )
1741
+ if atom_name not in conformation:
1742
+ logging.warning(
1743
+ 'Missing atom "%s" for CCD "%s"',
1744
+ atom_name,
1745
+ all_token_atoms_layout.res_name[idx],
1746
+ )
1747
+ ref = conformation.get(atom_name, _DEFAULT_BLANK_REF)
1748
+ for k in ref:
1749
+ result[k][idx] = ref[k]
1750
+
1751
+ # Assign a unique reference space id to each component, to determine which
1752
+ # reference positions live in the same reference space.
1753
+ space_str_id = (
1754
+ all_token_atoms_layout.chain_id[idx],
1755
+ all_token_atoms_layout.res_id[idx],
1756
+ )
1757
+ if space_str_id not in ref_space_uids:
1758
+ ref_space_uids[space_str_id] = len(ref_space_uids)
1759
+ result['ref_space_uid'][idx] = ref_space_uids[space_str_id]
1760
+
1761
+ if atom_names_all:
1762
+ atom_names_all = np.concatenate(atom_names_all, axis=0)
1763
+ res_ids_all = np.concatenate(res_ids_all, axis=0)
1764
+ chain_ids_all = np.concatenate(chain_ids_all, axis=0)
1765
+ if ligand_ligand_bonds is not None:
1766
+ adjusted_ligand_ligand_bonds = atom_layout.AtomLayout(
1767
+ atom_name=np.concatenate(
1768
+ [ligand_ligand_bonds.atom_name, atom_names_all], axis=0
1769
+ ),
1770
+ chain_id=np.concatenate(
1771
+ [ligand_ligand_bonds.chain_id, chain_ids_all], axis=0
1772
+ ),
1773
+ res_id=np.concatenate(
1774
+ [ligand_ligand_bonds.res_id, res_ids_all], axis=0
1775
+ ),
1776
+ )
1777
+ else:
1778
+ adjusted_ligand_ligand_bonds = atom_layout.AtomLayout(
1779
+ atom_name=atom_names_all,
1780
+ chain_id=chain_ids_all,
1781
+ res_id=res_ids_all,
1782
+ )
1783
+ else:
1784
+ adjusted_ligand_ligand_bonds = ligand_ligand_bonds
1785
+
1786
+ return cls(**result), adjusted_ligand_ligand_bonds
1787
+
1788
+ @classmethod
1789
+ def from_data_dict(cls, batch: BatchDict) -> Self:
1790
+ return cls(
1791
+ positions=batch['ref_pos'],
1792
+ mask=batch['ref_mask'],
1793
+ element=batch['ref_element'],
1794
+ charge=batch['ref_charge'],
1795
+ atom_name_chars=batch['ref_atom_name_chars'],
1796
+ ref_space_uid=batch['ref_space_uid'],
1797
+ )
1798
+
1799
+ def as_data_dict(self) -> BatchDict:
1800
+ return {
1801
+ 'ref_pos': self.positions,
1802
+ 'ref_mask': self.mask,
1803
+ 'ref_element': self.element,
1804
+ 'ref_charge': self.charge,
1805
+ 'ref_atom_name_chars': self.atom_name_chars,
1806
+ 'ref_space_uid': self.ref_space_uid,
1807
+ }
1808
+
1809
+
1810
+ jax.tree_util.register_dataclass(
1811
+ RefStructure,
1812
+ data_fields=[f.name for f in dataclasses.fields(RefStructure)],
1813
+ meta_fields=[],
1814
+ )
1815
+
1816
+
1817
+ @dataclasses.dataclass(frozen=True)
1818
+ class ConvertModelOutput:
1819
+ """Contains atom layout info."""
1820
+
1821
+ cleaned_struc: structure.Structure
1822
+ token_atoms_layout: atom_layout.AtomLayout
1823
+ flat_output_layout: atom_layout.AtomLayout
1824
+ empty_output_struc: structure.Structure
1825
+ polymer_ligand_bonds: atom_layout.AtomLayout
1826
+ ligand_ligand_bonds: atom_layout.AtomLayout
1827
+
1828
+ @classmethod
1829
+ def compute_features(
1830
+ cls,
1831
+ all_token_atoms_layout: atom_layout.AtomLayout,
1832
+ padding_shapes: PaddingShapes,
1833
+ cleaned_struc: structure.Structure,
1834
+ flat_output_layout: atom_layout.AtomLayout,
1835
+ empty_output_struc: structure.Structure,
1836
+ polymer_ligand_bonds: atom_layout.AtomLayout,
1837
+ ligand_ligand_bonds: atom_layout.AtomLayout,
1838
+ ) -> Self:
1839
+ """Pads the all_token_atoms_layout and stores other data."""
1840
+ # Crop and pad the all_token_atoms_layout.
1841
+ token_atoms_layout = all_token_atoms_layout.copy_and_pad_to(
1842
+ (padding_shapes.num_tokens, all_token_atoms_layout.shape[1])
1843
+ )
1844
+
1845
+ return cls(
1846
+ cleaned_struc=cleaned_struc,
1847
+ token_atoms_layout=token_atoms_layout,
1848
+ flat_output_layout=flat_output_layout,
1849
+ empty_output_struc=empty_output_struc,
1850
+ polymer_ligand_bonds=polymer_ligand_bonds,
1851
+ ligand_ligand_bonds=ligand_ligand_bonds,
1852
+ )
1853
+
1854
+ @classmethod
1855
+ def from_data_dict(cls, batch: BatchDict) -> Self:
1856
+ """Construct atom layout object from dictionary."""
1857
+
1858
+ return cls(
1859
+ cleaned_struc=_unwrap(batch.get('cleaned_struc', None)),
1860
+ token_atoms_layout=_unwrap(batch.get('token_atoms_layout', None)),
1861
+ flat_output_layout=_unwrap(batch.get('flat_output_layout', None)),
1862
+ empty_output_struc=_unwrap(batch.get('empty_output_struc', None)),
1863
+ polymer_ligand_bonds=_unwrap(batch.get('polymer_ligand_bonds', None)),
1864
+ ligand_ligand_bonds=_unwrap(batch.get('ligand_ligand_bonds', None)),
1865
+ )
1866
+
1867
+ def as_data_dict(self) -> BatchDict:
1868
+ return {
1869
+ 'cleaned_struc': np.array(self.cleaned_struc, object),
1870
+ 'token_atoms_layout': np.array(self.token_atoms_layout, object),
1871
+ 'flat_output_layout': np.array(self.flat_output_layout, object),
1872
+ 'empty_output_struc': np.array(self.empty_output_struc, object),
1873
+ 'polymer_ligand_bonds': np.array(self.polymer_ligand_bonds, object),
1874
+ 'ligand_ligand_bonds': np.array(self.ligand_ligand_bonds, object),
1875
+ }
1876
+
1877
+
1878
+ jax.tree_util.register_dataclass(
1879
+ ConvertModelOutput,
1880
+ data_fields=[f.name for f in dataclasses.fields(ConvertModelOutput)],
1881
+ meta_fields=[],
1882
+ )
1883
+
1884
+
1885
+ @dataclasses.dataclass(frozen=True)
1886
+ class AtomCrossAtt:
1887
+ """Operate on flat atoms."""
1888
+
1889
+ token_atoms_to_queries: atom_layout.GatherInfo
1890
+ tokens_to_queries: atom_layout.GatherInfo
1891
+ tokens_to_keys: atom_layout.GatherInfo
1892
+ queries_to_keys: atom_layout.GatherInfo
1893
+ queries_to_token_atoms: atom_layout.GatherInfo
1894
+
1895
+ @classmethod
1896
+ def compute_features(
1897
+ cls,
1898
+ all_token_atoms_layout: atom_layout.AtomLayout, # (num_tokens, num_dense)
1899
+ queries_subset_size: int,
1900
+ keys_subset_size: int,
1901
+ padding_shapes: PaddingShapes,
1902
+ ) -> Self:
1903
+ """Computes gather indices and meta data to work with a flat atom list."""
1904
+
1905
+ token_atoms_layout = all_token_atoms_layout.copy_and_pad_to(
1906
+ (padding_shapes.num_tokens, all_token_atoms_layout.shape[1])
1907
+ )
1908
+ token_atoms_mask = token_atoms_layout.atom_name.astype(bool)
1909
+ flat_layout = token_atoms_layout[token_atoms_mask]
1910
+ num_atoms = flat_layout.shape[0]
1911
+
1912
+ padded_flat_layout = flat_layout.copy_and_pad_to((
1913
+ padding_shapes.num_atoms,
1914
+ ))
1915
+
1916
+ # Create the layout for queries
1917
+ num_subsets = padding_shapes.num_atoms // queries_subset_size
1918
+ lay_arr = padded_flat_layout.to_array()
1919
+ queries_layout = atom_layout.AtomLayout.from_array(
1920
+ lay_arr.reshape((6, num_subsets, queries_subset_size))
1921
+ )
1922
+
1923
+ # Create the layout for the keys (the key subsets are centered around the
1924
+ # query subsets)
1925
+ # Create initial gather indices (contain out-of-bound indices)
1926
+ subset_centers = np.arange(
1927
+ queries_subset_size / 2, padding_shapes.num_atoms, queries_subset_size
1928
+ )
1929
+ flat_to_key_gathers = (
1930
+ subset_centers[:, None]
1931
+ + np.arange(-keys_subset_size / 2, keys_subset_size / 2)[None, :]
1932
+ )
1933
+ flat_to_key_gathers = flat_to_key_gathers.astype(int)
1934
+ # Shift subsets with out-of-bound indices, such that they are fully within
1935
+ # the bounds.
1936
+ for row in range(flat_to_key_gathers.shape[0]):
1937
+ if flat_to_key_gathers[row, 0] < 0:
1938
+ flat_to_key_gathers[row, :] -= flat_to_key_gathers[row, 0]
1939
+ elif flat_to_key_gathers[row, -1] > num_atoms - 1:
1940
+ overflow = flat_to_key_gathers[row, -1] - (num_atoms - 1)
1941
+ flat_to_key_gathers[row, :] -= overflow
1942
+ # Create the keys layout.
1943
+ keys_layout = padded_flat_layout[flat_to_key_gathers]
1944
+
1945
+ # Create gather indices for conversion between token atoms layout,
1946
+ # queries layout and keys layout.
1947
+ token_atoms_to_queries = atom_layout.compute_gather_idxs(
1948
+ source_layout=token_atoms_layout, target_layout=queries_layout
1949
+ )
1950
+
1951
+ token_atoms_to_keys = atom_layout.compute_gather_idxs(
1952
+ source_layout=token_atoms_layout, target_layout=keys_layout
1953
+ )
1954
+
1955
+ queries_to_keys = atom_layout.compute_gather_idxs(
1956
+ source_layout=queries_layout, target_layout=keys_layout
1957
+ )
1958
+
1959
+ queries_to_token_atoms = atom_layout.compute_gather_idxs(
1960
+ source_layout=queries_layout, target_layout=token_atoms_layout
1961
+ )
1962
+
1963
+ # Create gather indices for conversion of tokens layout to
1964
+ # queries and keys layout
1965
+ token_idxs = np.arange(padding_shapes.num_tokens).astype(np.int64)
1966
+ token_idxs = np.broadcast_to(token_idxs[:, None], token_atoms_layout.shape)
1967
+ tokens_to_queries = atom_layout.GatherInfo(
1968
+ gather_idxs=atom_layout.convert(
1969
+ token_atoms_to_queries, token_idxs, layout_axes=(0, 1)
1970
+ ),
1971
+ gather_mask=atom_layout.convert(
1972
+ token_atoms_to_queries, token_atoms_mask, layout_axes=(0, 1)
1973
+ ),
1974
+ input_shape=np.array((padding_shapes.num_tokens,)),
1975
+ )
1976
+
1977
+ tokens_to_keys = atom_layout.GatherInfo(
1978
+ gather_idxs=atom_layout.convert(
1979
+ token_atoms_to_keys, token_idxs, layout_axes=(0, 1)
1980
+ ),
1981
+ gather_mask=atom_layout.convert(
1982
+ token_atoms_to_keys, token_atoms_mask, layout_axes=(0, 1)
1983
+ ),
1984
+ input_shape=np.array((padding_shapes.num_tokens,)),
1985
+ )
1986
+
1987
+ return cls(
1988
+ token_atoms_to_queries=token_atoms_to_queries,
1989
+ tokens_to_queries=tokens_to_queries,
1990
+ tokens_to_keys=tokens_to_keys,
1991
+ queries_to_keys=queries_to_keys,
1992
+ queries_to_token_atoms=queries_to_token_atoms,
1993
+ )
1994
+
1995
+ @classmethod
1996
+ def from_data_dict(cls, batch: BatchDict) -> Self:
1997
+ return cls(
1998
+ token_atoms_to_queries=atom_layout.GatherInfo.from_dict(
1999
+ batch, key_prefix='token_atoms_to_queries'
2000
+ ),
2001
+ tokens_to_queries=atom_layout.GatherInfo.from_dict(
2002
+ batch, key_prefix='tokens_to_queries'
2003
+ ),
2004
+ tokens_to_keys=atom_layout.GatherInfo.from_dict(
2005
+ batch, key_prefix='tokens_to_keys'
2006
+ ),
2007
+ queries_to_keys=atom_layout.GatherInfo.from_dict(
2008
+ batch, key_prefix='queries_to_keys'
2009
+ ),
2010
+ queries_to_token_atoms=atom_layout.GatherInfo.from_dict(
2011
+ batch, key_prefix='queries_to_token_atoms'
2012
+ ),
2013
+ )
2014
+
2015
+ def as_data_dict(self) -> BatchDict:
2016
+ return {
2017
+ **self.token_atoms_to_queries.as_dict(
2018
+ key_prefix='token_atoms_to_queries'
2019
+ ),
2020
+ **self.tokens_to_queries.as_dict(key_prefix='tokens_to_queries'),
2021
+ **self.tokens_to_keys.as_dict(key_prefix='tokens_to_keys'),
2022
+ **self.queries_to_keys.as_dict(key_prefix='queries_to_keys'),
2023
+ **self.queries_to_token_atoms.as_dict(
2024
+ key_prefix='queries_to_token_atoms'
2025
+ ),
2026
+ }
2027
+
2028
+
2029
+ jax.tree_util.register_dataclass(
2030
+ AtomCrossAtt,
2031
+ data_fields=[f.name for f in dataclasses.fields(AtomCrossAtt)],
2032
+ meta_fields=[],
2033
+ )
2034
+
2035
+
2036
+ @dataclasses.dataclass(frozen=True)
2037
+ class Frames:
2038
+ """Features for backbone frames."""
2039
+
2040
+ mask: xnp_ndarray
2041
+
2042
+ @classmethod
2043
+ def compute_features(
2044
+ cls,
2045
+ all_tokens: atom_layout.AtomLayout,
2046
+ all_token_atoms_layout: atom_layout.AtomLayout,
2047
+ ref_structure: RefStructure,
2048
+ padding_shapes: PaddingShapes,
2049
+ ) -> Self:
2050
+ """Computes features for backbone frames."""
2051
+ num_tokens = padding_shapes.num_tokens
2052
+ all_token_atoms_layout = all_token_atoms_layout.copy_and_pad_to(
2053
+ (num_tokens, all_token_atoms_layout.shape[1])
2054
+ )
2055
+
2056
+ all_token_atoms_to_all_tokens = atom_layout.compute_gather_idxs(
2057
+ source_layout=all_token_atoms_layout, target_layout=all_tokens
2058
+ )
2059
+ ref_coordinates = atom_layout.convert(
2060
+ all_token_atoms_to_all_tokens,
2061
+ ref_structure.positions.astype(np.float32),
2062
+ layout_axes=(0, 1),
2063
+ )
2064
+ ref_mask = atom_layout.convert(
2065
+ all_token_atoms_to_all_tokens,
2066
+ ref_structure.mask.astype(bool),
2067
+ layout_axes=(0, 1),
2068
+ )
2069
+ ref_mask = ref_mask & all_token_atoms_to_all_tokens.gather_mask.astype(bool)
2070
+
2071
+ all_frame_mask = []
2072
+
2073
+ # Iterate over tokens
2074
+ for idx, args in enumerate(
2075
+ zip(all_tokens.chain_type, all_tokens.chain_id, all_tokens.res_id)
2076
+ ):
2077
+
2078
+ chain_type, chain_id, res_id = args
2079
+
2080
+ if chain_type in list(mmcif_names.PEPTIDE_CHAIN_TYPES):
2081
+ frame_mask = True
2082
+ elif chain_type in list(mmcif_names.NUCLEIC_ACID_CHAIN_TYPES):
2083
+ frame_mask = True
2084
+ elif chain_type in list(mmcif_names.NON_POLYMER_CHAIN_TYPES):
2085
+ # For ligands, build frames from closest atoms from the same molecule.
2086
+ (local_token_idxs,) = np.where(
2087
+ (all_tokens.chain_type == chain_type)
2088
+ & (all_tokens.chain_id == chain_id)
2089
+ & (all_tokens.res_id == res_id)
2090
+ )
2091
+
2092
+ if len(local_token_idxs) < 3:
2093
+ frame_mask = False
2094
+
2095
+ else:
2096
+ # [local_tokens]
2097
+ local_dist = np.linalg.norm(
2098
+ ref_coordinates[idx] - ref_coordinates[local_token_idxs], axis=-1
2099
+ )
2100
+ local_mask = ref_mask[local_token_idxs]
2101
+ cost = local_dist + 1e8 * ~local_mask
2102
+ cost = cost + 1e8 * (idx == local_token_idxs)
2103
+ # [local_tokens]
2104
+ closest_idxs = np.argsort(cost, axis=0)
2105
+
2106
+ # The closest indices index an array of local tokens. Convert this
2107
+ # to indices of the full (num_tokens,) array.
2108
+ global_closest_idxs = local_token_idxs[closest_idxs]
2109
+
2110
+ # Construct frame by placing the current token at the origin and two
2111
+ # nearest atoms on either side.
2112
+ global_frame_idxs = np.array(
2113
+ (global_closest_idxs[0], idx, global_closest_idxs[1])
2114
+ )
2115
+
2116
+ # Check that the frame atoms are not colinear.
2117
+ a, b, c = ref_coordinates[global_frame_idxs]
2118
+ vec1 = a - b
2119
+ vec2 = c - b
2120
+ # Reference coordinates can be all zeros, in which case we have
2121
+ # to explicitly set colinearity.
2122
+ if np.isclose(np.linalg.norm(vec1, axis=-1), 0) or np.isclose(
2123
+ np.linalg.norm(vec2, axis=-1), 0
2124
+ ):
2125
+ is_colinear = True
2126
+ logging.info('Found identical coordinates: Assigning as colinear.')
2127
+ else:
2128
+ vec1 = vec1 / np.linalg.norm(vec1, axis=-1)
2129
+ vec2 = vec2 / np.linalg.norm(vec2, axis=-1)
2130
+ cos_angle = np.einsum('...k,...k->...', vec1, vec2)
2131
+ # <25 degree deviation is considered colinear.
2132
+ is_colinear = 1 - np.abs(cos_angle) < 0.0937
2133
+
2134
+ frame_mask = not is_colinear
2135
+ else:
2136
+ # No frame for other chain types.
2137
+ frame_mask = False
2138
+
2139
+ all_frame_mask.append(frame_mask)
2140
+
2141
+ all_frame_mask = np.array(all_frame_mask, dtype=bool)
2142
+
2143
+ mask = _pad_to(all_frame_mask, (padding_shapes.num_tokens,))
2144
+
2145
+ return cls(mask=mask)
2146
+
2147
+ @classmethod
2148
+ def from_data_dict(cls, batch: BatchDict) -> Self:
2149
+ return cls(mask=batch['frames_mask'])
2150
+
2151
+ def as_data_dict(self) -> BatchDict:
2152
+ return {'frames_mask': self.mask}
2153
+
2154
+
2155
+ jax.tree_util.register_dataclass(
2156
+ Frames,
2157
+ data_fields=[f.name for f in dataclasses.fields(Frames)],
2158
+ meta_fields=[],
2159
+ )
flax_model/alphafold3/model/mkdssp_pybind.cc ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2024 DeepMind Technologies Limited
2
+ //
3
+ // AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
4
+ // this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
5
+ //
6
+ // To request access to the AlphaFold 3 model parameters, follow the process set
7
+ // out at https://github.com/google-deepmind/alphafold3. You may only use these
8
+ // if received directly from Google. Use is subject to terms of use available at
9
+ // https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md
10
+
11
+ #include "alphafold3/model/mkdssp_pybind.h"
12
+
13
+ #include <filesystem>
14
+
15
+ #include <cif++/file.hpp>
16
+ #include <cif++/pdb.hpp>
17
+ #include <dssp.hpp>
18
+ #include <sstream>
19
+
20
+ #include "absl/strings/string_view.h"
21
+ #include "pybind11/pybind11.h"
22
+ #include "pybind11/pytypes.h"
23
+
24
+ namespace alphafold3 {
25
+ namespace py = pybind11;
26
+
27
+ void RegisterModuleMkdssp(pybind11::module m) {
28
+ py::module site = py::module::import("site");
29
+ py::list paths = py::cast<py::list>(site.attr("getsitepackages")());
30
+ // Find the first path that contains the libcifpp components.cif file.
31
+ bool found = false;
32
+ for (const auto& py_path : paths) {
33
+ auto path_str =
34
+ std::filesystem::path(py::cast<absl::string_view>(py_path)) /
35
+ "share/libcifpp/components.cif";
36
+ if (std::filesystem::exists(path_str)) {
37
+ setenv("LIBCIFPP_DATA_DIR", path_str.parent_path().c_str(), 0);
38
+ found = true;
39
+ break;
40
+ }
41
+ }
42
+ if (!found) {
43
+ throw py::type_error("Could not find the libcifpp components.cif file.");
44
+ }
45
+ m.def(
46
+ "get_dssp",
47
+ [](absl::string_view mmcif, int model_no,
48
+ int min_poly_proline_stretch_length,
49
+ bool calculate_surface_accessibility) {
50
+ cif::file cif_file(mmcif.data(), mmcif.size());
51
+ dssp result(cif_file.front(), model_no, min_poly_proline_stretch_length,
52
+ calculate_surface_accessibility);
53
+ std::stringstream sstream;
54
+ result.write_legacy_output(sstream);
55
+ return sstream.str();
56
+ },
57
+ py::arg("mmcif"), py::arg("model_no") = 1,
58
+ py::arg("min_poly_proline_stretch_length") = 3,
59
+ py::arg("calculate_surface_accessibility") = false,
60
+ py::doc("Gets secondary structure from an mmCIF file."));
61
+ }
62
+
63
+ } // namespace alphafold3
flax_model/alphafold3/model/mkdssp_pybind.h ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * Copyright 2024 DeepMind Technologies Limited
3
+ *
4
+ * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
5
+ * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
6
+ *
7
+ * To request access to the AlphaFold 3 model parameters, follow the process set
8
+ * out at https://github.com/google-deepmind/alphafold3. You may only use these
9
+ * if received directly from Google. Use is subject to terms of use available at
10
+ * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md
11
+ */
12
+
13
+ #ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_
14
+ #define ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_
15
+
16
+
17
+ #include "pybind11/pybind11.h"
18
+
19
+ namespace alphafold3 {
20
+
21
+ void RegisterModuleMkdssp(pybind11::module m);
22
+
23
+ }
24
+
25
+
26
+ #endif // ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_
flax_model/alphafold3/model/mmcif_metadata.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Adds mmCIF metadata (to be ModelCIF-conformant) and author and legal info."""
4
+
5
+ from typing import Final
6
+
7
+ from flax_model.alphafold3.structure import mmcif
8
+ import numpy as np
9
+
10
+ _LICENSE_URL: Final[str] = (
11
+ 'https://github.com/google-deepmind/alphafold3/blob/main/OUTPUT_TERMS_OF_USE.md'
12
+ )
13
+
14
+ _LICENSE: Final[str] = f"""
15
+ Non-commercial use only, by using this file you agree to the terms of use found
16
+ at {_LICENSE_URL}.
17
+ To request access to the AlphaFold 3 model parameters, follow the process set
18
+ out at https://github.com/google-deepmind/alphafold3. You may only use these if
19
+ received directly from Google. Use is subject to terms of use available at
20
+ https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md.
21
+ """.strip()
22
+
23
+ _DISCLAIMER: Final[str] = """\
24
+ AlphaFold 3 and its output are not intended for, have not been validated for,
25
+ and are not approved for clinical use. They are provided "as-is" without any
26
+ warranty of any kind, whether expressed or implied. No warranty is given that
27
+ use shall not infringe the rights of any third party.
28
+ """.strip()
29
+
30
+ _MMCIF_PAPER_AUTHORS: Final[tuple[str, ...]] = (
31
+ 'Google DeepMind',
32
+ 'Isomorphic Labs',
33
+ )
34
+
35
+ # Authors of the mmCIF - we set them to be equal to the authors of the paper.
36
+ _MMCIF_AUTHORS: Final[tuple[str, ...]] = _MMCIF_PAPER_AUTHORS
37
+
38
+
39
+ def add_metadata_to_mmcif(
40
+ old_cif: mmcif.Mmcif, version: str, model_id: bytes
41
+ ) -> mmcif.Mmcif:
42
+ """Adds metadata to a mmCIF to make it ModelCIF-conformant."""
43
+ cif = {}
44
+
45
+ # ModelCIF conformation dictionary.
46
+ cif['_audit_conform.dict_name'] = ['mmcif_ma.dic']
47
+ cif['_audit_conform.dict_version'] = ['1.4.5']
48
+ cif['_audit_conform.dict_location'] = [
49
+ 'https://raw.githubusercontent.com/ihmwg/ModelCIF/master/dist/mmcif_ma.dic'
50
+ ]
51
+
52
+ cif['_pdbx_data_usage.id'] = ['1', '2']
53
+ cif['_pdbx_data_usage.type'] = ['license', 'disclaimer']
54
+ cif['_pdbx_data_usage.details'] = [_LICENSE, _DISCLAIMER]
55
+ cif['_pdbx_data_usage.url'] = [_LICENSE_URL, '?']
56
+
57
+ # Structure author details.
58
+ cif['_audit_author.name'] = []
59
+ cif['_audit_author.pdbx_ordinal'] = []
60
+ for author_index, author_name in enumerate(_MMCIF_AUTHORS, start=1):
61
+ cif['_audit_author.name'].append(author_name)
62
+ cif['_audit_author.pdbx_ordinal'].append(str(author_index))
63
+
64
+ # Paper author details.
65
+ cif['_citation_author.citation_id'] = []
66
+ cif['_citation_author.name'] = []
67
+ cif['_citation_author.ordinal'] = []
68
+ for author_index, author_name in enumerate(_MMCIF_PAPER_AUTHORS, start=1):
69
+ cif['_citation_author.citation_id'].append('primary')
70
+ cif['_citation_author.name'].append(author_name)
71
+ cif['_citation_author.ordinal'].append(str(author_index))
72
+
73
+ # Paper citation details.
74
+ cif['_citation.id'] = ['primary']
75
+ cif['_citation.title'] = [
76
+ 'Accurate structure prediction of biomolecular interactions with'
77
+ ' AlphaFold 3'
78
+ ]
79
+ cif['_citation.journal_full'] = ['Nature']
80
+ cif['_citation.journal_volume'] = ['630']
81
+ cif['_citation.page_first'] = ['493']
82
+ cif['_citation.page_last'] = ['500']
83
+ cif['_citation.year'] = ['2024']
84
+ cif['_citation.journal_id_ASTM'] = ['NATUAS']
85
+ cif['_citation.country'] = ['UK']
86
+ cif['_citation.journal_id_ISSN'] = ['0028-0836']
87
+ cif['_citation.journal_id_CSD'] = ['0006']
88
+ cif['_citation.book_publisher'] = ['?']
89
+ cif['_citation.pdbx_database_id_PubMed'] = ['38718835']
90
+ cif['_citation.pdbx_database_id_DOI'] = ['10.1038/s41586-024-07487-w']
91
+
92
+ # Type of data in the dataset including data used in the model generation.
93
+ cif['_ma_data.id'] = ['1']
94
+ cif['_ma_data.name'] = ['Model']
95
+ cif['_ma_data.content_type'] = ['model coordinates']
96
+
97
+ # Description of number of instances for each entity.
98
+ cif['_ma_target_entity_instance.asym_id'] = old_cif['_struct_asym.id']
99
+ cif['_ma_target_entity_instance.entity_id'] = old_cif[
100
+ '_struct_asym.entity_id'
101
+ ]
102
+ cif['_ma_target_entity_instance.details'] = ['.'] * len(
103
+ cif['_ma_target_entity_instance.entity_id']
104
+ )
105
+
106
+ # Details about the target entities.
107
+ cif['_ma_target_entity.entity_id'] = cif[
108
+ '_ma_target_entity_instance.entity_id'
109
+ ]
110
+ cif['_ma_target_entity.data_id'] = ['1'] * len(
111
+ cif['_ma_target_entity.entity_id']
112
+ )
113
+ cif['_ma_target_entity.origin'] = ['.'] * len(
114
+ cif['_ma_target_entity.entity_id']
115
+ )
116
+
117
+ # Details of the models being deposited.
118
+ cif['_ma_model_list.ordinal_id'] = ['1']
119
+ cif['_ma_model_list.model_id'] = ['1']
120
+ cif['_ma_model_list.model_group_id'] = ['1']
121
+ cif['_ma_model_list.model_name'] = ['Top ranked model']
122
+
123
+ cif['_ma_model_list.model_group_name'] = [
124
+ f'AlphaFold-beta-20231127 ({version})'
125
+ ]
126
+ cif['_ma_model_list.data_id'] = ['1']
127
+ cif['_ma_model_list.model_type'] = ['Ab initio model']
128
+
129
+ # Software used.
130
+ cif['_software.pdbx_ordinal'] = ['1']
131
+ cif['_software.name'] = ['AlphaFold']
132
+ cif['_software.version'] = [
133
+ f'AlphaFold-beta-20231127 ({model_id.decode("ascii")})'
134
+ ]
135
+ cif['_software.type'] = ['package']
136
+ cif['_software.description'] = ['Structure prediction']
137
+ cif['_software.classification'] = ['other']
138
+ cif['_software.date'] = ['?']
139
+
140
+ # Collection of software into groups.
141
+ cif['_ma_software_group.ordinal_id'] = ['1']
142
+ cif['_ma_software_group.group_id'] = ['1']
143
+ cif['_ma_software_group.software_id'] = ['1']
144
+
145
+ # Method description to conform with ModelCIF.
146
+ cif['_ma_protocol_step.ordinal_id'] = ['1', '2', '3']
147
+ cif['_ma_protocol_step.protocol_id'] = ['1', '1', '1']
148
+ cif['_ma_protocol_step.step_id'] = ['1', '2', '3']
149
+ cif['_ma_protocol_step.method_type'] = [
150
+ 'coevolution MSA',
151
+ 'template search',
152
+ 'modeling',
153
+ ]
154
+
155
+ # Details of the metrics use to assess model confidence.
156
+ cif['_ma_qa_metric.id'] = ['1', '2']
157
+ cif['_ma_qa_metric.name'] = ['pLDDT', 'pLDDT']
158
+ # Accepted values are distance, energy, normalised score, other, zscore.
159
+ cif['_ma_qa_metric.type'] = ['pLDDT', 'pLDDT']
160
+ cif['_ma_qa_metric.mode'] = ['global', 'local']
161
+ cif['_ma_qa_metric.software_group_id'] = ['1', '1']
162
+
163
+ # Global model confidence pLDDT value.
164
+ cif['_ma_qa_metric_global.ordinal_id'] = ['1']
165
+ cif['_ma_qa_metric_global.model_id'] = ['1']
166
+ cif['_ma_qa_metric_global.metric_id'] = ['1']
167
+ # Mean over all atoms, since AlphaFold 3 outputs pLDDT per-atom.
168
+ global_plddt = np.mean(
169
+ [float(v) for v in old_cif['_atom_site.B_iso_or_equiv']]
170
+ )
171
+ cif['_ma_qa_metric_global.metric_value'] = [f'{global_plddt:.2f}']
172
+
173
+ # Local (per residue) model confidence pLDDT value.
174
+ cif['_ma_qa_metric_local.ordinal_id'] = []
175
+ cif['_ma_qa_metric_local.model_id'] = []
176
+ cif['_ma_qa_metric_local.label_asym_id'] = []
177
+ cif['_ma_qa_metric_local.label_seq_id'] = []
178
+ cif['_ma_qa_metric_local.label_comp_id'] = []
179
+ cif['_ma_qa_metric_local.metric_id'] = []
180
+ cif['_ma_qa_metric_local.metric_value'] = []
181
+
182
+ plddt_grouped_by_res = {}
183
+ for *res, atom_plddt in zip(
184
+ old_cif['_atom_site.label_asym_id'],
185
+ old_cif['_atom_site.label_seq_id'],
186
+ old_cif['_atom_site.label_comp_id'],
187
+ old_cif['_atom_site.B_iso_or_equiv'],
188
+ ):
189
+ plddt_grouped_by_res.setdefault(tuple(res), []).append(float(atom_plddt))
190
+
191
+ for ordinal_id, ((chain_id, res_id, res_name), res_plddts) in enumerate(
192
+ plddt_grouped_by_res.items(), start=1
193
+ ):
194
+ res_plddt = np.mean(res_plddts)
195
+ cif['_ma_qa_metric_local.ordinal_id'].append(str(ordinal_id))
196
+ cif['_ma_qa_metric_local.model_id'].append('1')
197
+ cif['_ma_qa_metric_local.label_asym_id'].append(chain_id)
198
+ cif['_ma_qa_metric_local.label_seq_id'].append(res_id)
199
+ cif['_ma_qa_metric_local.label_comp_id'].append(res_name)
200
+ cif['_ma_qa_metric_local.metric_id'].append('2') # See _ma_qa_metric.id.
201
+ cif['_ma_qa_metric_local.metric_value'].append(f'{res_plddt:.2f}')
202
+
203
+ cif['_atom_type.symbol'] = sorted(set(old_cif['_atom_site.type_symbol']))
204
+
205
+ return old_cif.copy_and_update(cif)
206
+
207
+
208
+ def add_legal_comment(cif: str) -> str:
209
+ """Adds legal comment at the top of the mmCIF."""
210
+ # fmt: off
211
+ # pylint: disable=line-too-long
212
+ comment = (
213
+ '# By using this file you agree to the legally binding terms of use found at\n'
214
+ f'# {_LICENSE_URL}.\n'
215
+ '# To request access to the AlphaFold 3 model parameters, follow the process set\n'
216
+ '# out at https://github.com/google-deepmind/alphafold3. You may only use these if\n'
217
+ '# received directly from Google. Use is subject to terms of use available at\n'
218
+ '# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md.'
219
+ )
220
+ # pylint: enable=line-too-long
221
+ # fmt: on
222
+ return f'{comment}\n{cif}'
flax_model/alphafold3/model/model.py ADDED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """AlphaFold3 model."""
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ import concurrent
7
+ import dataclasses
8
+ import functools
9
+ from typing import Any, TypeAlias
10
+
11
+ from absl import logging
12
+ from flax_model.alphafold3 import structure
13
+ from flax_model.alphafold3.common import base_config
14
+ from flax_model.alphafold3.model import confidences
15
+ from flax_model.alphafold3.model import feat_batch
16
+ from flax_model.alphafold3.model import features
17
+ from flax_model.alphafold3.model import model_config
18
+ from flax_model.alphafold3.model.atom_layout import atom_layout
19
+ from flax_model.alphafold3.model.components import mapping
20
+ from flax_model.alphafold3.model.components import utils
21
+ from flax_model.alphafold3.model.network import atom_cross_attention
22
+ from flax_model.alphafold3.model.network import confidence_head
23
+ from flax_model.alphafold3.model.network import diffusion_head
24
+ from flax_model.alphafold3.model.network import distogram_head
25
+ from flax_model.alphafold3.model.network import evoformer as evoformer_network
26
+ from flax_model.alphafold3.model.network import featurization
27
+ import haiku as hk
28
+ import jax
29
+ import jax.numpy as jnp
30
+ import numpy as np
31
+
32
+
33
+ ModelResult: TypeAlias = Mapping[str, Any]
34
+ _ScalarNumberOrArray: TypeAlias = Mapping[str, float | int | np.ndarray]
35
+
36
+
37
+ @dataclasses.dataclass(frozen=True)
38
+ class InferenceResult:
39
+ """Postprocessed model result.
40
+
41
+ Attributes:
42
+ predicted_structure: Predicted protein structure.
43
+ numerical_data: Useful numerical data (scalars or arrays) to be saved at
44
+ inference time.
45
+ metadata: Smaller numerical data (usually scalar) to be saved as inference
46
+ metadata.
47
+ debug_outputs: Additional dict for debugging, e.g. raw outputs of a model
48
+ forward pass.
49
+ model_id: Model identifier.
50
+ """
51
+
52
+ predicted_structure: structure.Structure = dataclasses.field()
53
+ numerical_data: _ScalarNumberOrArray = dataclasses.field(default_factory=dict)
54
+ metadata: _ScalarNumberOrArray = dataclasses.field(default_factory=dict)
55
+ debug_outputs: Mapping[str, Any] = dataclasses.field(default_factory=dict)
56
+ model_id: bytes = b''
57
+
58
+
59
+ def get_predicted_structure(
60
+ result: ModelResult, batch: feat_batch.Batch
61
+ ) -> structure.Structure:
62
+ """Creates the predicted structure and ion preditions.
63
+
64
+ Args:
65
+ result: model output in a model specific layout
66
+ batch: model input batch
67
+
68
+ Returns:
69
+ Predicted structure.
70
+ """
71
+ model_output_coords = result['diffusion_samples']['atom_positions']
72
+
73
+ # Rearrange model output coordinates to the flat output layout.
74
+ model_output_to_flat = atom_layout.compute_gather_idxs(
75
+ source_layout=batch.convert_model_output.token_atoms_layout,
76
+ target_layout=batch.convert_model_output.flat_output_layout,
77
+ )
78
+ pred_flat_atom_coords = atom_layout.convert(
79
+ gather_info=model_output_to_flat,
80
+ arr=model_output_coords,
81
+ layout_axes=(-3, -2),
82
+ )
83
+
84
+ predicted_lddt = result.get('predicted_lddt')
85
+
86
+ if predicted_lddt is not None:
87
+ pred_flat_b_factors = atom_layout.convert(
88
+ gather_info=model_output_to_flat,
89
+ arr=predicted_lddt,
90
+ layout_axes=(-2, -1),
91
+ )
92
+ else:
93
+ # Handle models which don't have predicted_lddt outputs.
94
+ pred_flat_b_factors = np.zeros(pred_flat_atom_coords.shape[:-1])
95
+
96
+ (missing_atoms_indices,) = np.nonzero(model_output_to_flat.gather_mask == 0)
97
+ if missing_atoms_indices.shape[0] > 0:
98
+ missing_atoms_flat_layout = batch.convert_model_output.flat_output_layout[
99
+ missing_atoms_indices
100
+ ]
101
+ missing_atoms_uids = list(
102
+ zip(
103
+ missing_atoms_flat_layout.chain_id,
104
+ missing_atoms_flat_layout.res_id,
105
+ missing_atoms_flat_layout.res_name,
106
+ missing_atoms_flat_layout.atom_name,
107
+ )
108
+ )
109
+ logging.warning(
110
+ 'Target %s: warning: %s atoms were not predicted by the '
111
+ 'model, setting their coordinates to (0, 0, 0). '
112
+ 'Missing atoms: %s',
113
+ batch.convert_model_output.empty_output_struc.name,
114
+ missing_atoms_indices.shape[0],
115
+ missing_atoms_uids,
116
+ )
117
+
118
+ # Put them into a structure
119
+ pred_struc = batch.convert_model_output.empty_output_struc
120
+ pred_struc = pred_struc.copy_and_update_atoms(
121
+ atom_x=pred_flat_atom_coords[..., 0],
122
+ atom_y=pred_flat_atom_coords[..., 1],
123
+ atom_z=pred_flat_atom_coords[..., 2],
124
+ atom_b_factor=pred_flat_b_factors,
125
+ atom_occupancy=np.ones(pred_flat_atom_coords.shape[:-1]), # Always 1.0.
126
+ )
127
+ # Set manually/differently when adding metadata.
128
+ pred_struc = pred_struc.copy_and_update_globals(release_date=None)
129
+ return pred_struc
130
+
131
+
132
+ def create_target_feat_embedding(
133
+ batch: feat_batch.Batch,
134
+ config: evoformer_network.Evoformer.Config,
135
+ global_config: model_config.GlobalConfig,
136
+ ) -> jnp.ndarray:
137
+ """Create target feature embedding."""
138
+
139
+ dtype = jnp.bfloat16 if global_config.bfloat16 == 'all' else jnp.float32
140
+
141
+ with utils.bfloat16_context():
142
+ target_feat = featurization.create_target_feat(
143
+ batch,
144
+ append_per_atom_features=False,
145
+ ).astype(dtype)
146
+
147
+ enc = atom_cross_attention.atom_cross_att_encoder(
148
+ token_atoms_act=None,
149
+ trunk_single_cond=None,
150
+ trunk_pair_cond=None,
151
+ config=config.per_atom_conditioning,
152
+ global_config=global_config,
153
+ batch=batch,
154
+ name='evoformer_conditioning',
155
+ )
156
+ target_feat = jnp.concatenate([target_feat, enc.token_act], axis=-1).astype(
157
+ dtype
158
+ )
159
+
160
+ return target_feat
161
+
162
+
163
+ def _compute_ptm(
164
+ result: ModelResult,
165
+ num_tokens: int,
166
+ asym_id: np.ndarray,
167
+ pae_single_mask: np.ndarray,
168
+ interface: bool,
169
+ ) -> np.ndarray:
170
+ """Computes the pTM metrics from PAE."""
171
+ return np.stack(
172
+ [
173
+ confidences.predicted_tm_score(
174
+ tm_adjusted_pae=tm_adjusted_pae[:num_tokens, :num_tokens],
175
+ asym_id=asym_id,
176
+ pair_mask=pae_single_mask[:num_tokens, :num_tokens],
177
+ interface=interface,
178
+ )
179
+ for tm_adjusted_pae in result['tmscore_adjusted_pae_global']
180
+ ],
181
+ axis=0,
182
+ )
183
+
184
+
185
+ def _compute_chain_pair_iptm(
186
+ num_tokens: int,
187
+ asym_ids: np.ndarray,
188
+ mask: np.ndarray,
189
+ tm_adjusted_pae: np.ndarray,
190
+ ) -> np.ndarray:
191
+ """Computes the chain pair ipTM metrics from PAE."""
192
+ return np.stack(
193
+ [
194
+ confidences.chain_pairwise_predicted_tm_scores(
195
+ tm_adjusted_pae=sample_tm_adjusted_pae[:num_tokens],
196
+ asym_id=asym_ids[:num_tokens],
197
+ pair_mask=mask[:num_tokens, :num_tokens],
198
+ )
199
+ for sample_tm_adjusted_pae in tm_adjusted_pae
200
+ ],
201
+ axis=0,
202
+ )
203
+
204
+
205
+ class Model(hk.Module):
206
+ """Full model. Takes in data batch and returns model outputs."""
207
+
208
+ class HeadsConfig(base_config.BaseConfig):
209
+ diffusion: diffusion_head.DiffusionHead.Config = base_config.autocreate()
210
+ confidence: confidence_head.ConfidenceHead.Config = base_config.autocreate()
211
+ distogram: distogram_head.DistogramHead.Config = base_config.autocreate()
212
+
213
+ class Config(base_config.BaseConfig):
214
+ evoformer: evoformer_network.Evoformer.Config = base_config.autocreate()
215
+ global_config: model_config.GlobalConfig = base_config.autocreate()
216
+ heads: 'Model.HeadsConfig' = base_config.autocreate()
217
+ num_recycles: int = 10
218
+ return_embeddings: bool = False
219
+ return_distogram: bool = False
220
+
221
+ def __init__(self, config: Config, name: str = 'diffuser'):
222
+ super().__init__(name=name)
223
+ self.config = config
224
+ self.global_config = config.global_config
225
+ self.diffusion_module = diffusion_head.DiffusionHead(
226
+ self.config.heads.diffusion, self.global_config
227
+ )
228
+
229
+ @hk.transparent
230
+ def _sample_diffusion(
231
+ self,
232
+ batch: feat_batch.Batch,
233
+ embeddings: dict[str, jnp.ndarray],
234
+ *,
235
+ sample_config: diffusion_head.SampleConfig,
236
+ ) -> dict[str, jnp.ndarray]:
237
+ denoising_step = functools.partial(
238
+ self.diffusion_module,
239
+ batch=batch,
240
+ embeddings=embeddings,
241
+ use_conditioning=True,
242
+ )
243
+
244
+ sample = diffusion_head.sample(
245
+ denoising_step=denoising_step,
246
+ batch=batch,
247
+ key=hk.next_rng_key(),
248
+ config=sample_config,
249
+ )
250
+ return sample
251
+
252
+ def __call__(
253
+ self, batch: features.BatchDict, key: jax.Array | None = None
254
+ ) -> ModelResult:
255
+ if key is None:
256
+ key = hk.next_rng_key()
257
+
258
+ batch = feat_batch.Batch.from_data_dict(batch)
259
+
260
+ embedding_module = evoformer_network.Evoformer(
261
+ self.config.evoformer, self.global_config
262
+ )
263
+ target_feat = create_target_feat_embedding(
264
+ batch=batch,
265
+ config=embedding_module.config,
266
+ global_config=self.global_config,
267
+ )
268
+
269
+ num_res = batch.num_res
270
+ if num_res>=4000:
271
+ def recycle_body(_, args):
272
+ prev, key = args
273
+ key, subkey = jax.random.split(key)
274
+ embeddings = embedding_module(
275
+ batch=batch,
276
+ prev=prev,
277
+ target_feat=target_feat,
278
+ key=subkey,
279
+ )
280
+ embeddings['pair'] = embeddings['pair'].astype(jnp.bfloat16)
281
+ embeddings['single'] = embeddings['single'].astype(jnp.bfloat16)
282
+ return embeddings, key
283
+
284
+ embeddings = {
285
+ 'pair': jnp.zeros(
286
+ [num_res, num_res, self.config.evoformer.pair_channel],
287
+ dtype=jnp.bfloat16,
288
+ ),
289
+ 'single': jnp.zeros(
290
+ [num_res, self.config.evoformer.seq_channel], dtype=jnp.bfloat16
291
+ ),
292
+ 'target_feat': target_feat,
293
+ }
294
+ else:
295
+ def recycle_body(_, args):
296
+ prev, key = args
297
+ key, subkey = jax.random.split(key)
298
+ embeddings = embedding_module(
299
+ batch=batch,
300
+ prev=prev,
301
+ target_feat=target_feat,
302
+ key=subkey,
303
+ )
304
+ embeddings['pair'] = embeddings['pair'].astype(jnp.float32)
305
+ embeddings['single'] = embeddings['single'].astype(jnp.float32)
306
+ return embeddings, key
307
+
308
+ embeddings = {
309
+ 'pair': jnp.zeros(
310
+ [num_res, num_res, self.config.evoformer.pair_channel],
311
+ dtype=jnp.float32,
312
+ ),
313
+ 'single': jnp.zeros(
314
+ [num_res, self.config.evoformer.seq_channel], dtype=jnp.float32
315
+ ),
316
+ 'target_feat': target_feat,
317
+ }
318
+ if hk.running_init():
319
+ embeddings, _ = recycle_body(None, (embeddings, key))
320
+ else:
321
+ # Number of recycles is number of additional forward trunk passes.
322
+ num_iter = self.config.num_recycles + 1
323
+ embeddings, _ = hk.fori_loop(0, num_iter, recycle_body, (embeddings, key))
324
+
325
+ samples = self._sample_diffusion(
326
+ batch,
327
+ embeddings,
328
+ sample_config=self.config.heads.diffusion.eval,
329
+ )
330
+
331
+ # Compute dist_error_fn over all samples for distance error logging.
332
+ confidence_output = mapping.sharded_map(
333
+ lambda dense_atom_positions: confidence_head.ConfidenceHead(
334
+ self.config.heads.confidence, self.global_config
335
+ )(
336
+ dense_atom_positions=dense_atom_positions,
337
+ embeddings=embeddings,
338
+ seq_mask=batch.token_features.mask,
339
+ token_atoms_to_pseudo_beta=batch.pseudo_beta_info.token_atoms_to_pseudo_beta,
340
+ asym_id=batch.token_features.asym_id,
341
+ ),
342
+ in_axes=0,
343
+ )(samples['atom_positions'])
344
+
345
+ distogram = distogram_head.DistogramHead(
346
+ self.config.heads.distogram, self.global_config
347
+ )(batch, embeddings, return_distogram=self.config.return_distogram)
348
+
349
+ output = {
350
+ 'diffusion_samples': samples,
351
+ 'distogram': distogram,
352
+ **confidence_output,
353
+ }
354
+ if self.config.return_embeddings:
355
+ output['single_embeddings'] = embeddings['single']
356
+ output['pair_embeddings'] = embeddings['pair']
357
+ return output
358
+
359
+ @classmethod
360
+ def get_inference_result(
361
+ cls,
362
+ batch: features.BatchDict,
363
+ result: ModelResult,
364
+ target_name: str = '',
365
+ ) -> Iterable[InferenceResult]:
366
+ """Get the predicted structure, scalars, and arrays for inference.
367
+
368
+ This function also computes any inference-time quantities, which are not a
369
+ part of the forward-pass, e.g. additional confidence scores. Note that this
370
+ function is not serialized, so it should be slim if possible.
371
+
372
+ Args:
373
+ batch: data batch used for model inference, incl. TPU invalid types.
374
+ result: output dict from the model's forward pass.
375
+ target_name: target name to be saved within structure.
376
+
377
+ Yields:
378
+ inference_result: dataclass object that contains a predicted structure,
379
+ important inference-time scalars and arrays, as well as a slightly trimmed
380
+ dictionary of raw model result from the forward pass (for debugging).
381
+ """
382
+ del target_name
383
+ batch = feat_batch.Batch.from_data_dict(batch)
384
+
385
+ # Retrieve structure and construct a predicted structure.
386
+ pred_structure = get_predicted_structure(result=result, batch=batch)
387
+
388
+ num_tokens = batch.token_features.seq_length.item()
389
+
390
+ pae_single_mask = np.tile(
391
+ batch.frames.mask[:, None],
392
+ [1, batch.frames.mask.shape[0]],
393
+ )
394
+ ptm = _compute_ptm(
395
+ result=result,
396
+ num_tokens=num_tokens,
397
+ asym_id=batch.token_features.asym_id[:num_tokens],
398
+ pae_single_mask=pae_single_mask,
399
+ interface=False,
400
+ )
401
+ iptm = _compute_ptm(
402
+ result=result,
403
+ num_tokens=num_tokens,
404
+ asym_id=batch.token_features.asym_id[:num_tokens],
405
+ pae_single_mask=pae_single_mask,
406
+ interface=True,
407
+ )
408
+ ptm_iptm_average = 0.8 * iptm + 0.2 * ptm
409
+
410
+ asym_ids = batch.token_features.asym_id[:num_tokens]
411
+ # Map asym IDs back to chain IDs. Asym IDs are constructed from chain IDs by
412
+ # iterating over the chain IDs, and for each unique chain ID incrementing
413
+ # the asym ID by 1 and mapping it to the particular chain ID. Asym IDs are
414
+ # 1-indexed, so subtract 1 to get back to the chain ID.
415
+ chain_ids = [pred_structure.chains[asym_id - 1] for asym_id in asym_ids]
416
+ res_ids = batch.token_features.residue_index[:num_tokens]
417
+
418
+ if len(np.unique(asym_ids[:num_tokens])) > 1:
419
+ # There is more than one chain, hence interface pTM (i.e. ipTM) defined,
420
+ # so use it.
421
+ ranking_confidence = ptm_iptm_average
422
+ else:
423
+ # There is only one chain, hence ipTM=NaN, so use just pTM.
424
+ ranking_confidence = ptm
425
+
426
+ contact_probs = result['distogram']['contact_probs']
427
+ # Compute PAE related summaries.
428
+ _, chain_pair_pae_min, _ = confidences.chain_pair_pae(
429
+ num_tokens=num_tokens,
430
+ asym_ids=batch.token_features.asym_id,
431
+ full_pae=result['full_pae'],
432
+ mask=pae_single_mask,
433
+ )
434
+ chain_pair_pde_mean, chain_pair_pde_min = confidences.chain_pair_pde(
435
+ num_tokens=num_tokens,
436
+ asym_ids=batch.token_features.asym_id,
437
+ full_pde=result['full_pde'],
438
+ )
439
+ intra_chain_single_pde, cross_chain_single_pde, _ = confidences.pde_single(
440
+ num_tokens,
441
+ batch.token_features.asym_id,
442
+ result['full_pde'],
443
+ contact_probs,
444
+ )
445
+ pae_metrics = confidences.pae_metrics(
446
+ num_tokens=num_tokens,
447
+ asym_ids=batch.token_features.asym_id,
448
+ full_pae=result['full_pae'],
449
+ mask=pae_single_mask,
450
+ contact_probs=contact_probs,
451
+ tm_adjusted_pae=result['tmscore_adjusted_pae_interface'],
452
+ )
453
+ ranking_confidence_pae = confidences.rank_metric(
454
+ result['full_pae'],
455
+ contact_probs * batch.frames.mask[:, None].astype(float),
456
+ )
457
+ chain_pair_iptm = _compute_chain_pair_iptm(
458
+ num_tokens=num_tokens,
459
+ asym_ids=batch.token_features.asym_id,
460
+ mask=pae_single_mask,
461
+ tm_adjusted_pae=result['tmscore_adjusted_pae_interface'],
462
+ )
463
+ # iptm_ichain is a vector of per-chain ptm values. iptm_ichain[0],
464
+ # for example, is just the zeroth diagonal entry of the chain pair iptm
465
+ # matrix:
466
+ # [[x, , ],
467
+ # [ , , ],
468
+ # [ , , ]]]
469
+ iptm_ichain = chain_pair_iptm.diagonal(axis1=-2, axis2=-1)
470
+ # iptm_xchain is a vector of cross-chain interactions for each chain.
471
+ # iptm_xchain[0], for example, is an average of chain 0's interactions with
472
+ # other chains:
473
+ # [[ ,x,x],
474
+ # [x, , ],
475
+ # [x, , ]]]
476
+ iptm_xchain = confidences.get_iptm_xchain(chain_pair_iptm)
477
+
478
+ predicted_distance_errors = result['average_pde']
479
+
480
+ # Computing solvent accessible area with dssp can be slow for large
481
+ # structures with lots of chains, so we parallelize the call.
482
+ pred_structures = pred_structure.unstack()
483
+ num_workers = len(pred_structures)
484
+ with concurrent.futures.ThreadPoolExecutor(
485
+ max_workers=num_workers
486
+ ) as executor:
487
+ has_clash = list(executor.map(confidences.has_clash, pred_structures))
488
+ fraction_disordered = list(
489
+ executor.map(confidences.fraction_disordered, pred_structures)
490
+ )
491
+
492
+ for idx, pred_structure in enumerate(pred_structures):
493
+ ranking_score = confidences.get_ranking_score(
494
+ ptm=ptm[idx],
495
+ iptm=iptm[idx],
496
+ fraction_disordered_=fraction_disordered[idx],
497
+ has_clash_=has_clash[idx],
498
+ )
499
+ yield InferenceResult(
500
+ predicted_structure=pred_structure,
501
+ numerical_data={
502
+ 'full_pde': result['full_pde'][idx, :num_tokens, :num_tokens],
503
+ 'full_pae': result['full_pae'][idx, :num_tokens, :num_tokens],
504
+ 'contact_probs': contact_probs[:num_tokens, :num_tokens],
505
+ },
506
+ metadata={
507
+ 'predicted_distance_error': predicted_distance_errors[idx],
508
+ 'ranking_score': ranking_score,
509
+ 'fraction_disordered': fraction_disordered[idx],
510
+ 'has_clash': has_clash[idx],
511
+ 'predicted_tm_score': ptm[idx],
512
+ 'interface_predicted_tm_score': iptm[idx],
513
+ 'chain_pair_pde_mean': chain_pair_pde_mean[idx],
514
+ 'chain_pair_pde_min': chain_pair_pde_min[idx],
515
+ 'chain_pair_pae_min': chain_pair_pae_min[idx],
516
+ 'ptm': ptm[idx],
517
+ 'iptm': iptm[idx],
518
+ 'ptm_iptm_average': ptm_iptm_average[idx],
519
+ 'intra_chain_single_pde': intra_chain_single_pde[idx],
520
+ 'cross_chain_single_pde': cross_chain_single_pde[idx],
521
+ 'pae_ichain': pae_metrics['pae_ichain'][idx],
522
+ 'pae_xchain': pae_metrics['pae_xchain'][idx],
523
+ 'ranking_confidence': ranking_confidence[idx],
524
+ 'ranking_confidence_pae': ranking_confidence_pae[idx],
525
+ 'chain_pair_iptm': chain_pair_iptm[idx],
526
+ 'iptm_ichain': iptm_ichain[idx],
527
+ 'iptm_xchain': iptm_xchain[idx],
528
+ 'token_chain_ids': chain_ids,
529
+ 'token_res_ids': res_ids,
530
+ },
531
+ model_id=result['__identifier__'],
532
+ debug_outputs={},
533
+ )
flax_model/alphafold3/model/model_config.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Global config for the model."""
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Literal, TypeAlias
7
+
8
+ from flax_model.alphafold3.common import base_config
9
+ from flax_model.alphafold3.jax.attention import attention
10
+
11
+
12
+ _Shape2DType: TypeAlias = tuple[int | None, int | None]
13
+
14
+
15
+ class GlobalConfig(base_config.BaseConfig):
16
+ bfloat16: Literal['all', 'none', 'intermediate'] = 'all'
17
+ final_init: Literal['zeros', 'linear'] = 'zeros'
18
+ pair_attention_chunk_size: Sequence[_Shape2DType] = ((1536, 128), (None, 32))
19
+ pair_transition_shard_spec: Sequence[_Shape2DType] = (
20
+ (2048, None),
21
+ (None, 1024),
22
+ )
23
+ # Note: flash_attention_implementation = 'xla' means no flash attention.
24
+ flash_attention_implementation: attention.Implementation = 'triton'
flax_model/alphafold3/model/msa_pairing.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Functions for producing "paired" and "unpaired" MSA features for each chain.
4
+
5
+ The paired MSA:
6
+ - Is made from the result of the all_seqs MSA query.
7
+ - Is ordered such that you can concatenate features across chains and related
8
+ sequences will end up on the same row. Related here means "from the same
9
+ species". Gaps are added to facilitate this whenever a sequence has no
10
+ suitable pair.
11
+
12
+ The unpaired MSA:
13
+ - Is made from the results of the remaining MSA queries.
14
+ - Has no special ordering properties.
15
+ - Is deduplicated such that it doesn't contain any sequences in the paired MSA.
16
+ """
17
+
18
+ from typing import Mapping, MutableMapping, Sequence
19
+ from flax_model.alphafold3.model import data_constants
20
+ import numpy as np
21
+
22
+
23
+ def _align_species(
24
+ all_species: Sequence[bytes],
25
+ chains_species_to_rows: Sequence[Mapping[bytes, np.ndarray]],
26
+ min_hits_per_species: Mapping[bytes, int],
27
+ ) -> np.ndarray:
28
+ """Aligns MSA row indices based on species.
29
+
30
+ Within a species, MSAs are aligned based on their original order (the first
31
+ sequence for a species in the first chain's MSA is aligned to the first
32
+ sequence for the same species in the second chain's MSA).
33
+
34
+ Args:
35
+ all_species: A list of all unique species identifiers.
36
+ chains_species_to_rows: A dictionary for each chain, that maps species to
37
+ the set of MSA row indices from that species in that chain.
38
+ min_hits_per_species: A mapping from species id, to the minimum MSA size
39
+ across chains for that species (ignoring chains with zero hits).
40
+
41
+ Returns:
42
+ A matrix of size [num_msa_rows, num_chains], where the i,j element is an
43
+ index into the jth chains MSA. Each row consists of sequences from each
44
+ chain for the same species (or -1 if that chain has no sequences for that
45
+ species).
46
+ """
47
+ # Each species block is of size [num_seqs x num_chains] and consists of
48
+ # indices into the respective MSAs that have been aligned and are all for the
49
+ # same species.
50
+ species_blocks = []
51
+ for species in all_species:
52
+ chain_row_indices = []
53
+ for species_to_rows in chains_species_to_rows:
54
+ min_msa_size = min_hits_per_species[species]
55
+ if species not in species_to_rows:
56
+ # If a given chain has no hits for a species then we pad it with -1's,
57
+ # later on these values are used to make sure each feature is padded
58
+ # with its appropriate pad value.
59
+ row_indices = np.full(min_msa_size, fill_value=-1, dtype=np.int32)
60
+ else:
61
+ # We crop down to the smallest MSA for a given species across chains.
62
+ row_indices = species_to_rows[species][:min_msa_size]
63
+ chain_row_indices.append(row_indices)
64
+ species_block = np.stack(chain_row_indices, axis=1)
65
+ species_blocks.append(species_block)
66
+ aligned_matrix = np.concatenate(species_blocks, axis=0)
67
+ return aligned_matrix
68
+
69
+
70
+ def create_paired_features(
71
+ chains: Sequence[MutableMapping[str, np.ndarray]],
72
+ max_paired_sequences: int,
73
+ nonempty_chain_ids: set[str],
74
+ max_hits_per_species: int,
75
+ ) -> Sequence[MutableMapping[str, np.ndarray]]:
76
+ """Creates per-chain MSA features where the MSAs have been aligned.
77
+
78
+ Args:
79
+ chains: A list of feature dicts, one for each chain.
80
+ max_paired_sequences: No more than this many paired sequences will be
81
+ returned from this function.
82
+ nonempty_chain_ids: A set of chain ids (str) that are included in the crop
83
+ there is no reason to process chains not in this list.
84
+ max_hits_per_species: No more than this number of sequences will be returned
85
+ for a given species.
86
+
87
+ Returns:
88
+ An updated feature dictionary for each chain, where the {}_all_seq features
89
+ have been aligned so that the nth row in chain 1 is aligned to the nth row
90
+ in chain 2's features.
91
+ """
92
+ # The number of chains that the given species appears in - we rank hits
93
+ # across more chains higher.
94
+ species_num_chains = {}
95
+
96
+ # For each chain we keep a mapping from species to the row indices in the
97
+ # original MSA for that chain.
98
+ chains_species_to_rows = []
99
+
100
+ # Keep track of the minimum number of hits across chains for a given species.
101
+ min_hits_per_species = {}
102
+
103
+ for chain in chains:
104
+ species_ids = chain['msa_species_identifiers_all_seq']
105
+
106
+ # The query gets an empty species_id, so no pairing happens for this row.
107
+ if (
108
+ species_ids.size == 0
109
+ or (species_ids.size == 1 and not species_ids[0])
110
+ or chain['chain_id'] not in nonempty_chain_ids
111
+ ):
112
+ chains_species_to_rows.append({})
113
+ continue
114
+
115
+ # For each species keep track of which row indices in the original MSA are
116
+ # from this species.
117
+ row_indices = np.arange(len(species_ids))
118
+ # The grouping np.split code requires that the input is already clustered
119
+ # by species id.
120
+ sort_idxs = species_ids.argsort()
121
+ species_ids = species_ids[sort_idxs]
122
+ row_indices = row_indices[sort_idxs]
123
+
124
+ species, unique_row_indices = np.unique(species_ids, return_index=True)
125
+ grouped_row_indices = np.split(row_indices, unique_row_indices[1:])
126
+ species_to_rows = dict(zip(species, grouped_row_indices, strict=True))
127
+ chains_species_to_rows.append(species_to_rows)
128
+
129
+ for s in species:
130
+ species_num_chains[s] = species_num_chains.get(s, 0) + 1
131
+
132
+ for species, row_indices in species_to_rows.items():
133
+ min_hits_per_species[species] = min(
134
+ min_hits_per_species.get(species, max_hits_per_species),
135
+ len(row_indices),
136
+ )
137
+
138
+ # Construct a mapping from the number of chains a species appears in to
139
+ # the list of species with that count.
140
+ num_chains_to_species = {}
141
+ for species, num_chains in species_num_chains.items():
142
+ if not species or num_chains <= 1:
143
+ continue
144
+ if num_chains not in num_chains_to_species:
145
+ num_chains_to_species[num_chains] = []
146
+ num_chains_to_species[num_chains].append(species)
147
+
148
+ num_rows_seen = 0
149
+ # We always keep the first row as it is the query sequence.
150
+ all_rows = [np.array([[0] * len(chains)], dtype=np.int32)]
151
+
152
+ # We prioritize species that have hits across more chains.
153
+ for num_chains in sorted(num_chains_to_species, reverse=True):
154
+ all_species = num_chains_to_species[num_chains]
155
+
156
+ # Align all the per-chain row indices by species, so every paired row is
157
+ # for a single species.
158
+ rows = _align_species(
159
+ all_species, chains_species_to_rows, min_hits_per_species
160
+ )
161
+ # Sort rows by the product of the original indices in the respective chain
162
+ # MSAS, so as to rank hits that appear higher in the original MSAs higher.
163
+ rank_metric = np.abs(np.prod(rows.astype(np.float32), axis=1))
164
+ sorted_rows = rows[np.argsort(rank_metric), :]
165
+ all_rows.append(sorted_rows)
166
+ num_rows_seen += rows.shape[0]
167
+ if num_rows_seen >= max_paired_sequences:
168
+ break
169
+
170
+ all_rows = np.concatenate(all_rows, axis=0)
171
+ all_rows = all_rows[:max_paired_sequences, :]
172
+
173
+ # Now we just have to select the relevant rows from the original msa and
174
+ # deletion matrix features
175
+ paired_chains = []
176
+ for chain_idx, chain in enumerate(chains):
177
+ out_chain = {k: v for k, v in chain.items() if 'all_seq' not in k}
178
+ selected_row_indices = all_rows[:, chain_idx]
179
+ for feat_name in {'msa', 'deletion_matrix'}:
180
+ all_seq_name = f'{feat_name}_all_seq'
181
+ feat_value = chain[all_seq_name]
182
+
183
+ # The selected row indices are padded to be the same shape for each chain,
184
+ # they are padded with -1's, so we add a single row onto the feature with
185
+ # the appropriate pad value. This has the effect that we correctly pad
186
+ # each feature since all padded indices will select this padding row.
187
+ pad_value = data_constants.MSA_PAD_VALUES[feat_name]
188
+ feat_value = np.concatenate([
189
+ feat_value,
190
+ np.full((1, feat_value.shape[1]), pad_value, feat_value.dtype),
191
+ ])
192
+
193
+ feat_value = feat_value[selected_row_indices, :]
194
+ out_chain[all_seq_name] = feat_value
195
+ out_chain['num_alignments_all_seq'] = np.array(
196
+ out_chain['msa_all_seq'].shape[0]
197
+ )
198
+ paired_chains.append(out_chain)
199
+ return paired_chains
200
+
201
+
202
+ def deduplicate_unpaired_sequences(
203
+ np_chains: Sequence[MutableMapping[str, np.ndarray]],
204
+ ) -> Sequence[MutableMapping[str, np.ndarray]]:
205
+ """Deduplicates unpaired sequences based on paired sequences."""
206
+
207
+ feature_names = np_chains[0].keys()
208
+ msa_features = (
209
+ data_constants.NUM_SEQ_MSA_FEATURES
210
+ + data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES
211
+ )
212
+
213
+ for chain in np_chains:
214
+ sequence_set = set(
215
+ hash(s.data.tobytes()) for s in chain['msa_all_seq'].astype(np.int8)
216
+ )
217
+ keep_rows = []
218
+ # Go through unpaired MSA seqs and remove any rows that correspond to the
219
+ # sequences that are already present in the paired MSA.
220
+ for row_num, seq in enumerate(chain['msa'].astype(np.int8)):
221
+ if hash(seq.data.tobytes()) not in sequence_set:
222
+ keep_rows.append(row_num)
223
+ for feature_name in feature_names:
224
+ if feature_name in msa_features:
225
+ chain[feature_name] = chain[feature_name][keep_rows]
226
+ chain['num_alignments'] = np.array(chain['msa'].shape[0], dtype=np.int32)
227
+ return np_chains
228
+
229
+
230
+ def choose_paired_unpaired_msa_crop_sizes(
231
+ unpaired_msa: np.ndarray,
232
+ paired_msa: np.ndarray | None,
233
+ total_msa_crop_size: int,
234
+ max_paired_sequences: int,
235
+ ) -> tuple[int, int | None]:
236
+ """Returns the sizes of the MSA crop and MSA_all_seq crop.
237
+
238
+ NOTE: Unpaired + paired MSA sizes can exceed total_msa_size when
239
+ there are lots of gapped rows. Through the pairing logic another chain(s)
240
+ will have fewer than total_msa_size.
241
+
242
+ Args:
243
+ unpaired_msa: The unpaired MSA array (not all_seq).
244
+ paired_msa: The paired MSA array (all_seq).
245
+ total_msa_crop_size: The maximum total number of sequences to crop to.
246
+ max_paired_sequences: The maximum number of sequences that can come from
247
+ MSA pairing.
248
+
249
+ Returns:
250
+ A tuple of:
251
+ The size of the reduced MSA crop (not all_seq features).
252
+ The size of the unreduced MSA crop (for all_seq features) or None, if
253
+ paired_msa is None.
254
+ """
255
+ if paired_msa is not None:
256
+ paired_crop_size = np.minimum(paired_msa.shape[0], max_paired_sequences)
257
+
258
+ # We reduce the number of un-paired sequences, by the number of times a
259
+ # sequence from this chains MSA is included in the paired MSA. This keeps
260
+ # the MSA size for each chain roughly constant.
261
+ cropped_all_seq_msa = paired_msa[:max_paired_sequences]
262
+ num_non_gapped_pairs = cropped_all_seq_msa.shape[0]
263
+
264
+ assert num_non_gapped_pairs <= max_paired_sequences
265
+ unpaired_crop_size = np.minimum(
266
+ unpaired_msa.shape[0], total_msa_crop_size - num_non_gapped_pairs
267
+ )
268
+ assert unpaired_crop_size >= 0
269
+ else:
270
+ unpaired_crop_size = np.minimum(unpaired_msa.shape[0], total_msa_crop_size)
271
+ paired_crop_size = None
272
+ return unpaired_crop_size, paired_crop_size
273
+
274
+
275
+ def remove_all_gapped_rows_from_all_seqs(
276
+ chains_list: Sequence[dict[str, np.ndarray]], asym_ids: Sequence[float]
277
+ ) -> Sequence[dict[str, np.ndarray]]:
278
+ """Removes all gapped rows from all_seq feat based on selected asym_ids."""
279
+
280
+ merged_msa_all_seq = np.concatenate(
281
+ [
282
+ chain['msa_all_seq']
283
+ for chain in chains_list
284
+ if chain['asym_id'][0] in asym_ids
285
+ ],
286
+ axis=1,
287
+ )
288
+
289
+ non_gapped_keep_rows = np.any(
290
+ merged_msa_all_seq != data_constants.MSA_GAP_IDX, axis=1
291
+ )
292
+ for chain in chains_list:
293
+ for feat_name in list(chains_list)[0]:
294
+ if '_all_seq' in feat_name:
295
+ feat_name_split = feat_name.split('_all_seq')[0]
296
+ if feat_name_split in (
297
+ data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES
298
+ + data_constants.NUM_SEQ_MSA_FEATURES
299
+ ):
300
+ # For consistency we do this for all chains even though the
301
+ # gapped rows are based on a selected set asym_ids.
302
+ chain[feat_name] = chain[feat_name][non_gapped_keep_rows]
303
+ chain['num_alignments_all_seq'] = np.sum(non_gapped_keep_rows)
304
+ return chains_list
flax_model/alphafold3/model/network/atom_cross_attention.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Per-atom cross attention."""
4
+ import dataclasses
5
+
6
+ from flax_model.alphafold3.common import base_config
7
+ from flax_model.alphafold3.model import feat_batch
8
+ from flax_model.alphafold3.model import model_config
9
+ from flax_model.alphafold3.model.atom_layout import atom_layout
10
+ from flax_model.alphafold3.model.components import haiku_modules as hm
11
+ from flax_model.alphafold3.model.components import utils
12
+ from flax_model.alphafold3.model.network import diffusion_transformer
13
+ # import chex
14
+ import jax
15
+ import jax.numpy as jnp
16
+
17
+
18
+ class AtomCrossAttEncoderConfig(base_config.BaseConfig):
19
+ per_token_channels: int = 768
20
+ per_atom_channels: int = 128
21
+ atom_transformer: diffusion_transformer.CrossAttTransformer.Config = (
22
+ base_config.autocreate(num_intermediate_factor=2, num_blocks=3)
23
+ )
24
+ per_atom_pair_channels: int = 16
25
+
26
+
27
+ def _per_atom_conditioning(
28
+ config: AtomCrossAttEncoderConfig, batch: feat_batch.Batch, name: str
29
+ ) -> tuple[jnp.ndarray, jnp.ndarray]:
30
+ """computes single and pair conditioning for all atoms in each token."""
31
+
32
+ c = config
33
+ # Compute per-atom single conditioning
34
+ # Shape (num_tokens, num_dense, channels)
35
+ act = hm.Linear(
36
+ c.per_atom_channels, precision='highest', name=f'{name}_embed_ref_pos'
37
+ )(batch.ref_structure.positions)
38
+ act += hm.Linear(c.per_atom_channels, name=f'{name}_embed_ref_mask')(
39
+ batch.ref_structure.mask.astype(jnp.float32)[:, :, None]
40
+ )
41
+ # Element is encoded as atomic number if the periodic table, so
42
+ # 128 should be fine.
43
+ act += hm.Linear(c.per_atom_channels, name=f'{name}_embed_ref_element')(
44
+ jax.nn.one_hot(batch.ref_structure.element, 128)
45
+ )
46
+ act += hm.Linear(c.per_atom_channels, name=f'{name}_embed_ref_charge')(
47
+ jnp.arcsinh(batch.ref_structure.charge)[:, :, None]
48
+ )
49
+ # Characters are encoded as ASCII code minus 32, so we need 64 classes,
50
+ # to encode all standard ASCII characters between 32 and 96.
51
+ atom_name_chars_1hot = jax.nn.one_hot(batch.ref_structure.atom_name_chars, 64)
52
+ num_token, num_dense, _ = act.shape
53
+ act += hm.Linear(c.per_atom_channels, name=f'{name}_embed_ref_atom_name')(
54
+ atom_name_chars_1hot.reshape(num_token, num_dense, -1)
55
+ )
56
+ act *= batch.ref_structure.mask.astype(jnp.float32)[:, :, None]
57
+
58
+ # Compute pair conditioning
59
+ # shape (num_tokens, num_dense, num_dense, channels)
60
+ # Embed single features
61
+ row_act = hm.Linear(
62
+ c.per_atom_pair_channels, name=f'{name}_single_to_pair_cond_row'
63
+ )(jax.nn.relu(act))
64
+ col_act = hm.Linear(
65
+ c.per_atom_pair_channels, name=f'{name}_single_to_pair_cond_col'
66
+ )(jax.nn.relu(act))
67
+ pair_act = row_act[:, :, None, :] + col_act[:, None, :, :]
68
+ # Embed pairwise offsets
69
+ pair_act += hm.Linear(
70
+ c.per_atom_pair_channels,
71
+ precision='highest',
72
+ name=f'{name}_embed_pair_offsets',
73
+ )(
74
+ batch.ref_structure.positions[:, :, None, :]
75
+ - batch.ref_structure.positions[:, None, :, :]
76
+ )
77
+ # Embed pairwise inverse squared distances
78
+ sq_dists = jnp.sum(
79
+ jnp.square(
80
+ batch.ref_structure.positions[:, :, None, :]
81
+ - batch.ref_structure.positions[:, None, :, :]
82
+ ),
83
+ axis=-1,
84
+ )
85
+ pair_act += hm.Linear(
86
+ c.per_atom_pair_channels, name=f'{name}_embed_pair_distances'
87
+ )(1.0 / (1 + sq_dists[:, :, :, None]))
88
+
89
+ return act, pair_act
90
+
91
+
92
+ @dataclasses.dataclass(frozen=True)
93
+ class AtomCrossAttEncoderOutput:
94
+ token_act: jnp.ndarray # (num_tokens, ch)
95
+ skip_connection: jnp.ndarray # (num_subsets, num_queries, ch)
96
+ queries_mask: jnp.ndarray # (num_subsets, num_queries)
97
+ queries_single_cond: jnp.ndarray # (num_subsets, num_queries, ch)
98
+ keys_mask: jnp.ndarray # (num_subsets, num_keys)
99
+ keys_single_cond: jnp.ndarray # (num_subsets, num_keys, ch)
100
+ pair_cond: jnp.ndarray # (num_subsets, num_queries, num_keys, ch)
101
+
102
+
103
+ jax.tree_util.register_dataclass(
104
+ AtomCrossAttEncoderOutput,
105
+ data_fields=[f.name for f in dataclasses.fields(AtomCrossAttEncoderOutput)],
106
+ meta_fields=[],
107
+ )
108
+
109
+
110
+ def atom_cross_att_encoder(
111
+ token_atoms_act: jnp.ndarray | None, # (num_tokens, max_atoms_per_token, 3)
112
+ trunk_single_cond: jnp.ndarray | None, # (num_tokens, ch)
113
+ trunk_pair_cond: jnp.ndarray | None, # (num_tokens, num_tokens, ch)
114
+ config: AtomCrossAttEncoderConfig,
115
+ global_config: model_config.GlobalConfig,
116
+ batch: feat_batch.Batch,
117
+ name: str,
118
+ ) -> AtomCrossAttEncoderOutput:
119
+ """Cross-attention on flat atom subsets and mapping to per-token features."""
120
+ c = config
121
+
122
+ # Compute single conditioning from atom meta data and convert to queries
123
+ # layout.
124
+ # (num_subsets, num_queries, channels)
125
+ token_atoms_single_cond, _ = _per_atom_conditioning(config, batch, name)
126
+ token_atoms_mask = batch.predicted_structure_info.atom_mask
127
+ queries_single_cond = atom_layout.convert(
128
+ batch.atom_cross_att.token_atoms_to_queries,
129
+ token_atoms_single_cond,
130
+ layout_axes=(-3, -2),
131
+ )
132
+ queries_mask = atom_layout.convert(
133
+ batch.atom_cross_att.token_atoms_to_queries,
134
+ token_atoms_mask,
135
+ layout_axes=(-2, -1),
136
+ )
137
+
138
+ # If provided, broadcast single conditioning from trunk to all queries
139
+ if trunk_single_cond is not None:
140
+ trunk_single_cond = hm.Linear(
141
+ c.per_atom_channels,
142
+ precision='highest',
143
+ initializer=global_config.final_init,
144
+ name=f'{name}_embed_trunk_single_cond',
145
+ )(
146
+ hm.LayerNorm(
147
+ use_fast_variance=False,
148
+ create_offset=False,
149
+ name=f'{name}_lnorm_trunk_single_cond',
150
+ )(trunk_single_cond)
151
+ )
152
+ queries_single_cond += atom_layout.convert(
153
+ batch.atom_cross_att.tokens_to_queries,
154
+ trunk_single_cond,
155
+ layout_axes=(-2,),
156
+ )
157
+
158
+ if token_atoms_act is None:
159
+ # if no token_atoms_act is given (e.g. begin of evoformer), we use the
160
+ # static conditioning only
161
+ queries_act = queries_single_cond
162
+ else:
163
+ # Convert token_atoms_act to queries layout and map to per_atom_channels
164
+ # (num_subsets, num_queries, channels)
165
+ queries_act = atom_layout.convert(
166
+ batch.atom_cross_att.token_atoms_to_queries,
167
+ token_atoms_act,
168
+ layout_axes=(-3, -2),
169
+ )
170
+ queries_act = hm.Linear(
171
+ c.per_atom_channels,
172
+ precision='highest',
173
+ name=f'{name}_atom_positions_to_features',
174
+ )(queries_act)
175
+ queries_act *= queries_mask[..., None]
176
+ queries_act += queries_single_cond
177
+
178
+ # Gather the keys from the queries.
179
+ keys_single_cond = atom_layout.convert(
180
+ batch.atom_cross_att.queries_to_keys,
181
+ queries_single_cond,
182
+ layout_axes=(-3, -2),
183
+ )
184
+ keys_mask = atom_layout.convert(
185
+ batch.atom_cross_att.queries_to_keys, queries_mask, layout_axes=(-2, -1)
186
+ )
187
+
188
+ # Embed single features into the pair conditioning.
189
+ # shape (num_subsets, num_queries, num_keys, ch)
190
+ row_act = hm.Linear(
191
+ c.per_atom_pair_channels, name=f'{name}_single_to_pair_cond_row'
192
+ )(jax.nn.relu(queries_single_cond))
193
+ pair_cond_keys_input = atom_layout.convert(
194
+ batch.atom_cross_att.queries_to_keys,
195
+ queries_single_cond,
196
+ layout_axes=(-3, -2),
197
+ )
198
+ col_act = hm.Linear(
199
+ c.per_atom_pair_channels, name=f'{name}_single_to_pair_cond_col'
200
+ )(jax.nn.relu(pair_cond_keys_input))
201
+ pair_act = row_act[:, :, None, :] + col_act[:, None, :, :]
202
+
203
+ if trunk_pair_cond is not None:
204
+ # If provided, broadcast the pair conditioning for the trunk (evoformer
205
+ # pairs) to the atom pair activations. This should boost ligands, but also
206
+ # help for cross attention within proteins, because we always have atoms
207
+ # from multiple residues in a subset.
208
+ # Map trunk pair conditioning to per_atom_pair_channels
209
+ # (num_tokens, num_tokens, per_atom_pair_channels)
210
+ trunk_pair_cond = hm.Linear(
211
+ c.per_atom_pair_channels,
212
+ precision='highest',
213
+ initializer=global_config.final_init,
214
+ name=f'{name}_embed_trunk_pair_cond',
215
+ )(
216
+ hm.LayerNorm(
217
+ use_fast_variance=False,
218
+ create_offset=False,
219
+ name=f'{name}_lnorm_trunk_pair_cond',
220
+ )(trunk_pair_cond)
221
+ )
222
+
223
+ # Create the GatherInfo into a flattened trunk_pair_cond from the
224
+ # queries and keys gather infos.
225
+ num_tokens = trunk_pair_cond.shape[0]
226
+ # (num_subsets, num_queries)
227
+ tokens_to_queries = batch.atom_cross_att.tokens_to_queries
228
+ # (num_subsets, num_keys)
229
+ tokens_to_keys = batch.atom_cross_att.tokens_to_keys
230
+ # (num_subsets, num_queries, num_keys)
231
+ trunk_pair_to_atom_pair = atom_layout.GatherInfo(
232
+ gather_idxs=(
233
+ num_tokens * tokens_to_queries.gather_idxs[:, :, None]
234
+ + tokens_to_keys.gather_idxs[:, None, :]
235
+ ),
236
+ gather_mask=(
237
+ tokens_to_queries.gather_mask[:, :, None]
238
+ & tokens_to_keys.gather_mask[:, None, :]
239
+ ),
240
+ input_shape=jnp.array((num_tokens, num_tokens)),
241
+ )
242
+ # Gather the conditioning and add it to the atom-pair activations.
243
+ pair_act += atom_layout.convert(
244
+ trunk_pair_to_atom_pair, trunk_pair_cond, layout_axes=(-3, -2)
245
+ )
246
+
247
+ # Embed pairwise offsets
248
+ queries_ref_pos = atom_layout.convert(
249
+ batch.atom_cross_att.token_atoms_to_queries,
250
+ batch.ref_structure.positions,
251
+ layout_axes=(-3, -2),
252
+ )
253
+ queries_ref_space_uid = atom_layout.convert(
254
+ batch.atom_cross_att.token_atoms_to_queries,
255
+ batch.ref_structure.ref_space_uid,
256
+ layout_axes=(-2, -1),
257
+ )
258
+ keys_ref_pos = atom_layout.convert(
259
+ batch.atom_cross_att.queries_to_keys,
260
+ queries_ref_pos,
261
+ layout_axes=(-3, -2),
262
+ )
263
+ keys_ref_space_uid = atom_layout.convert(
264
+ batch.atom_cross_att.queries_to_keys,
265
+ batch.ref_structure.ref_space_uid,
266
+ layout_axes=(-2, -1),
267
+ )
268
+
269
+ offsets_valid = (
270
+ queries_ref_space_uid[:, :, None] == keys_ref_space_uid[:, None, :]
271
+ )
272
+ offsets = queries_ref_pos[:, :, None, :] - keys_ref_pos[:, None, :, :]
273
+ pair_act += (
274
+ hm.Linear(
275
+ c.per_atom_pair_channels,
276
+ precision='highest',
277
+ name=f'{name}_embed_pair_offsets',
278
+ )(offsets)
279
+ * offsets_valid[:, :, :, None]
280
+ )
281
+
282
+ # Embed pairwise inverse squared distances
283
+ sq_dists = jnp.sum(jnp.square(offsets), axis=-1)
284
+ pair_act += (
285
+ hm.Linear(c.per_atom_pair_channels, name=f'{name}_embed_pair_distances')(
286
+ 1.0 / (1 + sq_dists[:, :, :, None])
287
+ )
288
+ * offsets_valid[:, :, :, None]
289
+ )
290
+ # Embed offsets valid mask
291
+ pair_act += hm.Linear(
292
+ c.per_atom_pair_channels, name=f'{name}_embed_pair_offsets_valid'
293
+ )(offsets_valid[:, :, :, None].astype(jnp.float32))
294
+
295
+ # Run a small MLP on the pair acitvations
296
+ pair_act2 = hm.Linear(
297
+ c.per_atom_pair_channels, initializer='relu', name=f'{name}_pair_mlp_1'
298
+ )(jax.nn.relu(pair_act))
299
+ pair_act2 = hm.Linear(
300
+ c.per_atom_pair_channels, initializer='relu', name=f'{name}_pair_mlp_2'
301
+ )(jax.nn.relu(pair_act2))
302
+ pair_act += hm.Linear(
303
+ c.per_atom_pair_channels,
304
+ initializer=global_config.final_init,
305
+ name=f'{name}_pair_mlp_3',
306
+ )(jax.nn.relu(pair_act2))
307
+
308
+ # Run the atom cross attention transformer.
309
+ queries_act = diffusion_transformer.CrossAttTransformer(
310
+ c.atom_transformer, global_config, name=f'{name}_atom_transformer_encoder'
311
+ )(
312
+ queries_act=queries_act,
313
+ queries_mask=queries_mask,
314
+ queries_to_keys=batch.atom_cross_att.queries_to_keys,
315
+ keys_mask=keys_mask,
316
+ queries_single_cond=queries_single_cond,
317
+ keys_single_cond=keys_single_cond,
318
+ pair_cond=pair_act,
319
+ )
320
+ queries_act *= queries_mask[..., None]
321
+ skip_connection = queries_act
322
+
323
+ # Convert back to token-atom layout and aggregate to tokens
324
+ queries_act = hm.Linear(
325
+ c.per_token_channels, name=f'{name}_project_atom_features_for_aggr'
326
+ )(queries_act)
327
+ token_atoms_act = atom_layout.convert(
328
+ batch.atom_cross_att.queries_to_token_atoms,
329
+ queries_act,
330
+ layout_axes=(-3, -2),
331
+ )
332
+ token_act = utils.mask_mean(
333
+ token_atoms_mask[..., None], jax.nn.relu(token_atoms_act), axis=-2
334
+ )
335
+
336
+ return AtomCrossAttEncoderOutput(
337
+ token_act=token_act,
338
+ skip_connection=skip_connection,
339
+ queries_mask=queries_mask,
340
+ queries_single_cond=queries_single_cond,
341
+ keys_mask=keys_mask,
342
+ keys_single_cond=keys_single_cond,
343
+ pair_cond=pair_act,
344
+ )
345
+
346
+
347
+ class AtomCrossAttDecoderConfig(base_config.BaseConfig):
348
+ per_atom_channels: int = 128
349
+ atom_transformer: diffusion_transformer.CrossAttTransformer.Config = (
350
+ base_config.autocreate(num_intermediate_factor=2, num_blocks=3)
351
+ )
352
+
353
+
354
+ def atom_cross_att_decoder(
355
+ token_act: jnp.ndarray, # (num_tokens, ch)
356
+ enc: AtomCrossAttEncoderOutput,
357
+ config: AtomCrossAttDecoderConfig,
358
+ global_config: model_config.GlobalConfig,
359
+ batch: feat_batch.Batch,
360
+ name: str,
361
+ ): # (num_tokens, max_atoms_per_token, 3)
362
+ """Mapping to per-atom features and self-attention on subsets."""
363
+ c = config
364
+ # map per-token act down to per_atom channels
365
+ token_act = hm.Linear(
366
+ c.per_atom_channels, name=f'{name}_project_token_features_for_broadcast'
367
+ )(token_act)
368
+ # Broadcast to token-atoms layout and convert to queries layout.
369
+ num_token, max_atoms_per_token = (
370
+ batch.atom_cross_att.queries_to_token_atoms.shape
371
+ )
372
+ token_atom_act = jnp.broadcast_to(
373
+ token_act[:, None, :],
374
+ (num_token, max_atoms_per_token, c.per_atom_channels),
375
+ )
376
+ queries_act = atom_layout.convert(
377
+ batch.atom_cross_att.token_atoms_to_queries,
378
+ token_atom_act,
379
+ layout_axes=(-3, -2),
380
+ )
381
+ queries_act += enc.skip_connection
382
+ queries_act *= enc.queries_mask[..., None]
383
+
384
+ # Run the atom cross attention transformer.
385
+ queries_act = diffusion_transformer.CrossAttTransformer(
386
+ c.atom_transformer, global_config, name=f'{name}_atom_transformer_decoder'
387
+ )(
388
+ queries_act=queries_act,
389
+ queries_mask=enc.queries_mask,
390
+ queries_to_keys=batch.atom_cross_att.queries_to_keys,
391
+ keys_mask=enc.keys_mask,
392
+ queries_single_cond=enc.queries_single_cond,
393
+ keys_single_cond=enc.keys_single_cond,
394
+ pair_cond=enc.pair_cond,
395
+ )
396
+ queries_act *= enc.queries_mask[..., None]
397
+ queries_act = hm.LayerNorm(
398
+ use_fast_variance=False,
399
+ create_offset=False,
400
+ name=f'{name}_atom_features_layer_norm',
401
+ )(queries_act)
402
+ queries_position_update = hm.Linear(
403
+ 3,
404
+ initializer=global_config.final_init,
405
+ precision='highest',
406
+ name=f'{name}_atom_features_to_position_update',
407
+ )(queries_act)
408
+ position_update = atom_layout.convert(
409
+ batch.atom_cross_att.queries_to_token_atoms,
410
+ queries_position_update,
411
+ layout_axes=(-3, -2),
412
+ )
413
+ return position_update
flax_model/alphafold3/model/network/confidence_head.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Confidence Head."""
4
+
5
+ from flax_model.alphafold3.common import base_config
6
+ from flax_model.alphafold3.model import model_config
7
+ from flax_model.alphafold3.model.atom_layout import atom_layout
8
+ from flax_model.alphafold3.model.components import haiku_modules as hm
9
+ from flax_model.alphafold3.model.components import utils
10
+ from flax_model.alphafold3.model.network import modules
11
+ from flax_model.alphafold3.model.network import template_modules
12
+ import haiku as hk
13
+ import jax
14
+ import jax.numpy as jnp
15
+
16
+
17
+ def _safe_norm(x, keepdims, axis, eps=1e-8):
18
+ return jnp.sqrt(eps + jnp.sum(jnp.square(x), axis=axis, keepdims=keepdims))
19
+
20
+
21
+ class ConfidenceHead(hk.Module):
22
+ """Head to predict the distance errors in a prediction."""
23
+
24
+ class PAEConfig(base_config.BaseConfig):
25
+ max_error_bin: float = 31.0
26
+ num_bins: int = 64
27
+
28
+ class Config(base_config.BaseConfig):
29
+ """Configuration for ConfidenceHead."""
30
+
31
+ pairformer: modules.PairFormerIteration.Config = base_config.autocreate(
32
+ single_attention=base_config.autocreate(),
33
+ single_transition=base_config.autocreate(),
34
+ num_layer=4,
35
+ )
36
+ max_error_bin: float = 31.0
37
+ num_plddt_bins: int = 50
38
+ num_bins: int = 64
39
+ no_embedding_prob: float = 0.2
40
+ pae: 'ConfidenceHead.PAEConfig' = base_config.autocreate()
41
+ dgram_features: template_modules.DistogramFeaturesConfig = (
42
+ base_config.autocreate()
43
+ )
44
+
45
+ def __init__(
46
+ self,
47
+ config: Config,
48
+ global_config: model_config.GlobalConfig,
49
+ name='confidence_head',
50
+ ):
51
+ super().__init__(name=name)
52
+ self.config = config
53
+ self.global_config = global_config
54
+
55
+ def _embed_features(
56
+ self,
57
+ dense_atom_positions,
58
+ token_atoms_to_pseudo_beta,
59
+ pair_mask,
60
+ pair_act,
61
+ target_feat,
62
+ ):
63
+ out = hm.Linear(pair_act.shape[-1], name='left_target_feat_project')(
64
+ target_feat
65
+ ).astype(pair_act.dtype)
66
+ out += hm.Linear(pair_act.shape[-1], name='right_target_feat_project')(
67
+ target_feat
68
+ ).astype(pair_act.dtype)[:, None]
69
+ positions = atom_layout.convert(
70
+ token_atoms_to_pseudo_beta,
71
+ dense_atom_positions,
72
+ layout_axes=(-3, -2),
73
+ )
74
+ dgram = template_modules.dgram_from_positions(
75
+ positions, self.config.dgram_features
76
+ )
77
+ dgram *= pair_mask[..., None]
78
+
79
+ out += hm.Linear(pair_act.shape[-1], name='distogram_feat_project')(
80
+ dgram.astype(pair_act.dtype)
81
+ )
82
+ return out
83
+
84
+ def __call__(
85
+ self,
86
+ dense_atom_positions: jnp.ndarray,
87
+ embeddings: dict[str, jnp.ndarray],
88
+ seq_mask: jnp.ndarray,
89
+ token_atoms_to_pseudo_beta: atom_layout.GatherInfo,
90
+ asym_id: jnp.ndarray,
91
+ ) -> dict[str, jnp.ndarray]:
92
+ """Builds ConfidenceHead module.
93
+
94
+ Arguments:
95
+ dense_atom_positions: [N_res, N_atom, 3] array of positions.
96
+ embeddings: Dictionary of representations.
97
+ seq_mask: Sequence mask.
98
+ token_atoms_to_pseudo_beta: Pseudo beta info for atom tokens.
99
+ asym_id: Asym ID token features.
100
+
101
+ Returns:
102
+ Dictionary of results.
103
+ """
104
+ dtype = (
105
+ jnp.bfloat16 if self.global_config.bfloat16 == 'all' else jnp.float32
106
+ )
107
+ with utils.bfloat16_context():
108
+ seq_mask_cast = seq_mask.astype(dtype)
109
+ pair_mask = seq_mask_cast[:, None] * seq_mask_cast[None, :]
110
+ pair_mask = pair_mask.astype(dtype)
111
+
112
+ pair_act = embeddings['pair'].astype(dtype)
113
+ single_act = embeddings['single'].astype(dtype)
114
+ target_feat = embeddings['target_feat'].astype(dtype)
115
+
116
+ num_residues = seq_mask.shape[0]
117
+ num_pair_channels = pair_act.shape[2]
118
+
119
+ pair_act += self._embed_features(
120
+ dense_atom_positions,
121
+ token_atoms_to_pseudo_beta,
122
+ pair_mask,
123
+ pair_act,
124
+ target_feat,
125
+ )
126
+
127
+ def pairformer_fn(act):
128
+ pair_act, single_act = act
129
+ return modules.PairFormerIteration(
130
+ self.config.pairformer,
131
+ self.global_config,
132
+ with_single=True,
133
+ name='confidence_pairformer',
134
+ )(
135
+ act=pair_act,
136
+ single_act=single_act,
137
+ pair_mask=pair_mask,
138
+ seq_mask=seq_mask,
139
+ )
140
+
141
+ pairformer_stack = hk.experimental.layer_stack(
142
+ self.config.pairformer.num_layer
143
+ )(pairformer_fn)
144
+
145
+ pair_act, single_act = pairformer_stack((pair_act, single_act))
146
+ pair_act = pair_act.astype(jnp.float32)
147
+ assert pair_act.shape == (num_residues, num_residues, num_pair_channels)
148
+
149
+ # Produce logits to predict a distogram of pairwise distance errors
150
+ # between the input prediction and the ground truth.
151
+
152
+ # Shape (num_res, num_res, num_bins)
153
+ left_distance_logits = hm.Linear(
154
+ self.config.num_bins,
155
+ initializer=self.global_config.final_init,
156
+ name='left_half_distance_logits',
157
+ )(hm.LayerNorm(name='logits_ln')(pair_act))
158
+ right_distance_logits = left_distance_logits
159
+ distance_logits = left_distance_logits + jnp.swapaxes( # Symmetrize.
160
+ right_distance_logits, -2, -3
161
+ )
162
+ # Shape (num_bins,)
163
+ distance_breaks = jnp.linspace(
164
+ 0.0, self.config.max_error_bin, self.config.num_bins - 1
165
+ )
166
+
167
+ step = distance_breaks[1] - distance_breaks[0]
168
+
169
+ # Add half-step to get the center
170
+ bin_centers = distance_breaks + step / 2
171
+ # Add a catch-all bin at the end.
172
+ bin_centers = jnp.concatenate(
173
+ [bin_centers, bin_centers[-1:] + step], axis=0
174
+ )
175
+
176
+ distance_probs = jax.nn.softmax(distance_logits, axis=-1)
177
+
178
+ pred_distance_error = (
179
+ jnp.sum(distance_probs * bin_centers, axis=-1) * pair_mask
180
+ )
181
+ average_pred_distance_error = jnp.sum(
182
+ pred_distance_error, axis=[-2, -1]
183
+ ) / jnp.sum(pair_mask, axis=[-2, -1])
184
+
185
+ # Predicted aligned error
186
+ pae_outputs = {}
187
+ # Shape (num_res, num_res, num_bins)
188
+ pae_logits = hm.Linear(
189
+ self.config.pae.num_bins,
190
+ initializer=self.global_config.final_init,
191
+ name='pae_logits',
192
+ )(hm.LayerNorm(name='pae_logits_ln')(pair_act))
193
+ # Shape (num_bins,)
194
+ pae_breaks = jnp.linspace(
195
+ 0.0, self.config.pae.max_error_bin, self.config.pae.num_bins - 1
196
+ )
197
+ step = pae_breaks[1] - pae_breaks[0]
198
+ # Add half-step to get the center
199
+ bin_centers = pae_breaks + step / 2
200
+ # Add a catch-all bin at the end.
201
+ bin_centers = jnp.concatenate(
202
+ [bin_centers, bin_centers[-1:] + step], axis=0
203
+ )
204
+ pae_probs = jax.nn.softmax(pae_logits, axis=-1)
205
+
206
+ seq_mask_bool = seq_mask.astype(bool)
207
+ pair_mask_bool = seq_mask_bool[:, None] * seq_mask_bool[None, :]
208
+ pae = jnp.sum(pae_probs * bin_centers, axis=-1) * pair_mask_bool
209
+ pae_outputs.update({
210
+ 'full_pae': pae,
211
+ })
212
+
213
+ # The pTM is computed outside of bfloat16 context.
214
+ tmscore_adjusted_pae_global, tmscore_adjusted_pae_interface = (
215
+ self._get_tmscore_adjusted_pae(
216
+ asym_id=asym_id,
217
+ seq_mask=seq_mask,
218
+ pair_mask=pair_mask_bool,
219
+ bin_centers=bin_centers,
220
+ pae_probs=pae_probs,
221
+ )
222
+ )
223
+ pae_outputs.update({
224
+ 'tmscore_adjusted_pae_global': tmscore_adjusted_pae_global,
225
+ 'tmscore_adjusted_pae_interface': tmscore_adjusted_pae_interface,
226
+ })
227
+ single_act = single_act.astype('float32')
228
+
229
+ # pLDDT
230
+ # Shape (num_res, num_atom, num_bins)
231
+ plddt_logits = hm.Linear(
232
+ (dense_atom_positions.shape[-2], self.config.num_plddt_bins),
233
+ initializer=self.global_config.final_init,
234
+ name='plddt_logits',
235
+ )(hm.LayerNorm(name='plddt_logits_ln')(single_act))
236
+
237
+ bin_width = 1.0 / self.config.num_plddt_bins
238
+ bin_centers = jnp.arange(0.5 * bin_width, 1.0, bin_width)
239
+ predicted_lddt = jnp.sum(
240
+ jax.nn.softmax(plddt_logits, axis=-1) * bin_centers, axis=-1
241
+ )
242
+ predicted_lddt = predicted_lddt * 100.0
243
+
244
+ # Experimentally resolved
245
+ # Shape (num_res, num_atom, 2)
246
+ experimentally_resolved_logits = hm.Linear(
247
+ (dense_atom_positions.shape[-2], 2),
248
+ initializer=self.global_config.final_init,
249
+ name='experimentally_resolved_logits',
250
+ )(hm.LayerNorm(name='experimentally_resolved_ln')(single_act))
251
+
252
+ predicted_experimentally_resolved = jax.nn.softmax(
253
+ experimentally_resolved_logits, axis=-1
254
+ )[..., 1]
255
+
256
+ return {
257
+ 'predicted_lddt': predicted_lddt,
258
+ 'predicted_experimentally_resolved': predicted_experimentally_resolved,
259
+ 'full_pde': pred_distance_error,
260
+ 'average_pde': average_pred_distance_error,
261
+ **pae_outputs,
262
+ }
263
+
264
+ def _get_tmscore_adjusted_pae(
265
+ self,
266
+ asym_id: jnp.ndarray,
267
+ seq_mask: jnp.ndarray,
268
+ pair_mask: jnp.ndarray,
269
+ bin_centers: jnp.ndarray,
270
+ pae_probs: jnp.ndarray,
271
+ ):
272
+ def get_tmscore_adjusted_pae(num_interface_tokens, bin_centers, pae_probs):
273
+ # Clip to avoid negative/undefined d0.
274
+ clipped_num_res = jnp.maximum(num_interface_tokens, 19)
275
+
276
+ # Compute d_0(num_res) as defined by TM-score, eqn. (5) in
277
+ # http://zhanglab.ccmb.med.umich.edu/papers/2004_3.pdf
278
+ # Yang & Skolnick "Scoring function for automated
279
+ # assessment of protein structure template quality" 2004.
280
+ d0 = 1.24 * (clipped_num_res - 15) ** (1.0 / 3) - 1.8
281
+
282
+ # Make compatible with [num_tokens, num_tokens, num_bins]
283
+ d0 = d0[:, :, None]
284
+ bin_centers = bin_centers[None, None, :]
285
+
286
+ # TM-Score term for every bin.
287
+ tm_per_bin = 1.0 / (1 + jnp.square(bin_centers) / jnp.square(d0))
288
+ # E_distances tm(distance).
289
+ predicted_tm_term = jnp.sum(pae_probs * tm_per_bin, axis=-1)
290
+ return predicted_tm_term
291
+
292
+ # Interface version
293
+ x = asym_id[None, :] == asym_id[:, None]
294
+ num_chain_tokens = jnp.sum(x * pair_mask, axis=-1)
295
+ num_interface_tokens = num_chain_tokens[None, :] + num_chain_tokens[:, None]
296
+ # Don't double-count within a single chain
297
+ num_interface_tokens -= x * (num_interface_tokens // 2)
298
+ num_interface_tokens = num_interface_tokens * pair_mask
299
+
300
+ num_global_tokens = jnp.full(
301
+ shape=pair_mask.shape, fill_value=seq_mask.sum()
302
+ )
303
+
304
+ assert num_global_tokens.dtype == 'int32'
305
+ assert num_interface_tokens.dtype == 'int32'
306
+ global_apae = get_tmscore_adjusted_pae(
307
+ num_global_tokens, bin_centers, pae_probs
308
+ )
309
+ interface_apae = get_tmscore_adjusted_pae(
310
+ num_interface_tokens, bin_centers, pae_probs
311
+ )
312
+ return global_apae, interface_apae
flax_model/alphafold3/model/network/diffusion_head.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Diffusion Head."""
4
+
5
+ from collections.abc import Callable
6
+
7
+ from flax_model.alphafold3.common import base_config
8
+ from flax_model.alphafold3.model import feat_batch
9
+ from flax_model.alphafold3.model import model_config
10
+ from flax_model.alphafold3.model.components import haiku_modules as hm
11
+ from flax_model.alphafold3.model.components import utils
12
+ from flax_model.alphafold3.model.network import atom_cross_attention
13
+ from flax_model.alphafold3.model.network import diffusion_transformer
14
+ from flax_model.alphafold3.model.network import featurization
15
+ from flax_model.alphafold3.model.network import noise_level_embeddings
16
+ #import chex
17
+ import haiku as hk
18
+ import jax
19
+ import jax.numpy as jnp
20
+
21
+
22
+ # Carefully measured by averaging multimer training set.
23
+ SIGMA_DATA = 16.0
24
+
25
+
26
+ def random_rotation(key):
27
+ # Create a random rotation (Gram-Schmidt orthogonalization of two
28
+ # random normal vectors)
29
+ v0, v1 = jax.random.normal(key, shape=(2, 3))
30
+ e0 = v0 / jnp.maximum(1e-10, jnp.linalg.norm(v0))
31
+ v1 = v1 - e0 * jnp.dot(v1, e0, precision=jax.lax.Precision.HIGHEST)
32
+ e1 = v1 / jnp.maximum(1e-10, jnp.linalg.norm(v1))
33
+ e2 = jnp.cross(e0, e1)
34
+ return jnp.stack([e0, e1, e2])
35
+
36
+
37
+ def random_augmentation(
38
+ rng_key: jnp.ndarray,
39
+ positions: jnp.ndarray,
40
+ mask: jnp.ndarray,
41
+ ) -> jnp.ndarray:
42
+ """Apply random rigid augmentation.
43
+
44
+ Args:
45
+ rng_key: random key
46
+ positions: atom positions of shape (<common_axes>, 3)
47
+ mask: per-atom mask of shape (<common_axes>,)
48
+
49
+ Returns:
50
+ Transformed positions with the same shape as input positions.
51
+ """
52
+ rotation_key, translation_key = jax.random.split(rng_key)
53
+
54
+ center = utils.mask_mean(
55
+ mask[..., None], positions, axis=(-2, -3), keepdims=True, eps=1e-6
56
+ )
57
+ rot = random_rotation(rotation_key)
58
+ translation = jax.random.normal(translation_key, shape=(3,))
59
+
60
+ augmented_positions = (
61
+ jnp.einsum(
62
+ '...i,ij->...j',
63
+ positions - center,
64
+ rot,
65
+ precision=jax.lax.Precision.HIGHEST,
66
+ )
67
+ + translation
68
+ )
69
+ return augmented_positions * mask[..., None]
70
+
71
+
72
+ def noise_schedule(t, smin=0.0004, smax=160.0, p=7):
73
+ return (
74
+ SIGMA_DATA
75
+ * (smax ** (1 / p) + t * (smin ** (1 / p) - smax ** (1 / p))) ** p
76
+ )
77
+
78
+
79
+ class ConditioningConfig(base_config.BaseConfig):
80
+ pair_channel: int
81
+ seq_channel: int
82
+ prob: float
83
+
84
+
85
+ class SampleConfig(base_config.BaseConfig):
86
+ steps: int
87
+ gamma_0: float = 0.8
88
+ gamma_min: float = 1.0
89
+ noise_scale: float = 1.003
90
+ step_scale: float = 1.5
91
+ num_samples: int = 1
92
+
93
+
94
+ class DiffusionHead(hk.Module):
95
+ """Denoising Diffusion Head."""
96
+
97
+ class Config(
98
+ atom_cross_attention.AtomCrossAttEncoderConfig,
99
+ atom_cross_attention.AtomCrossAttDecoderConfig,
100
+ ):
101
+ """Configuration for DiffusionHead."""
102
+
103
+ eval_batch_size: int = 5
104
+ eval_batch_dim_shard_size: int = 5
105
+ conditioning: ConditioningConfig = base_config.autocreate(
106
+ prob=0.8, pair_channel=128, seq_channel=384
107
+ )
108
+ eval: SampleConfig = base_config.autocreate(
109
+ num_samples=5,
110
+ steps=200,
111
+ )
112
+ transformer: diffusion_transformer.Transformer.Config = (
113
+ base_config.autocreate()
114
+ )
115
+
116
+ def __init__(
117
+ self,
118
+ config: Config,
119
+ global_config: model_config.GlobalConfig,
120
+ name='diffusion_head',
121
+ ):
122
+ self.config = config
123
+ self.global_config = global_config
124
+ super().__init__(name=name)
125
+
126
+ @hk.transparent
127
+ def _conditioning(
128
+ self,
129
+ batch: feat_batch.Batch,
130
+ embeddings: dict[str, jnp.ndarray],
131
+ noise_level: jnp.ndarray,
132
+ use_conditioning: bool,
133
+ ) -> tuple[jnp.ndarray, jnp.ndarray]:
134
+ single_embedding = use_conditioning * embeddings['single']
135
+ pair_embedding = use_conditioning * embeddings['pair']
136
+
137
+ rel_features = featurization.create_relative_encoding(
138
+ seq_features=batch.token_features,
139
+ max_relative_idx=32,
140
+ max_relative_chain=2,
141
+ ).astype(pair_embedding.dtype)
142
+ features_2d = jnp.concatenate([pair_embedding, rel_features], axis=-1)
143
+ pair_cond = hm.Linear(
144
+ self.config.conditioning.pair_channel,
145
+ precision='highest',
146
+ name='pair_cond_initial_projection',
147
+ )(
148
+ hm.LayerNorm(
149
+ use_fast_variance=False,
150
+ create_offset=False,
151
+ name='pair_cond_initial_norm',
152
+ )(features_2d)
153
+ )
154
+
155
+ for idx in range(2):
156
+ pair_cond += diffusion_transformer.transition_block(
157
+ pair_cond, 2, self.global_config, name=f'pair_transition_{idx}'
158
+ )
159
+
160
+ target_feat = embeddings['target_feat']
161
+ features_1d = jnp.concatenate([single_embedding, target_feat], axis=-1)
162
+ single_cond = hm.LayerNorm(
163
+ use_fast_variance=False,
164
+ create_offset=False,
165
+ name='single_cond_initial_norm',
166
+ )(features_1d)
167
+ single_cond = hm.Linear(
168
+ self.config.conditioning.seq_channel,
169
+ precision='highest',
170
+ name='single_cond_initial_projection',
171
+ )(single_cond)
172
+
173
+ noise_embedding = noise_level_embeddings.noise_embeddings(
174
+ sigma_scaled_noise_level=noise_level / SIGMA_DATA
175
+ )
176
+ single_cond += hm.Linear(
177
+ self.config.conditioning.seq_channel,
178
+ precision='highest',
179
+ name='noise_embedding_initial_projection',
180
+ )(
181
+ hm.LayerNorm(
182
+ use_fast_variance=False,
183
+ create_offset=False,
184
+ name='noise_embedding_initial_norm',
185
+ )(noise_embedding)
186
+ )
187
+
188
+ for idx in range(2):
189
+ single_cond += diffusion_transformer.transition_block(
190
+ single_cond, 2, self.global_config, name=f'single_transition_{idx}'
191
+ )
192
+
193
+ return single_cond, pair_cond
194
+
195
+ def __call__(
196
+ self,
197
+ # positions_noisy.shape: (num_token, max_atoms_per_token, 3)
198
+ positions_noisy: jnp.ndarray,
199
+ noise_level: jnp.ndarray,
200
+ batch: feat_batch.Batch,
201
+ embeddings: dict[str, jnp.ndarray],
202
+ use_conditioning: bool,
203
+ ) -> jnp.ndarray:
204
+
205
+ with utils.bfloat16_context():
206
+ # Get conditioning
207
+ trunk_single_cond, trunk_pair_cond = self._conditioning(
208
+ batch=batch,
209
+ embeddings=embeddings,
210
+ noise_level=noise_level,
211
+ use_conditioning=use_conditioning,
212
+ )
213
+
214
+ # Extract features
215
+ sequence_mask = batch.token_features.mask
216
+ atom_mask = batch.predicted_structure_info.atom_mask
217
+
218
+ # Position features
219
+ act = positions_noisy * atom_mask[..., None]
220
+ act = act / jnp.sqrt(noise_level**2 + SIGMA_DATA**2)
221
+
222
+ enc = atom_cross_attention.atom_cross_att_encoder(
223
+ token_atoms_act=act,
224
+ trunk_single_cond=embeddings['single'],
225
+ trunk_pair_cond=trunk_pair_cond,
226
+ config=self.config,
227
+ global_config=self.global_config,
228
+ batch=batch,
229
+ name='diffusion',
230
+ )
231
+ act = enc.token_act
232
+
233
+ # Token-token attention
234
+ # chex.assert_shape(act, (None, self.config.per_token_channels))
235
+ act = jnp.asarray(act, dtype=jnp.float32)
236
+
237
+ act += hm.Linear(
238
+ act.shape[-1],
239
+ precision='highest',
240
+ initializer=self.global_config.final_init,
241
+ name='single_cond_embedding_projection',
242
+ )(
243
+ hm.LayerNorm(
244
+ use_fast_variance=False,
245
+ create_offset=False,
246
+ name='single_cond_embedding_norm',
247
+ )(trunk_single_cond)
248
+ )
249
+
250
+ act = jnp.asarray(act, dtype=jnp.float32)
251
+ trunk_single_cond = jnp.asarray(trunk_single_cond, dtype=jnp.float32)
252
+ trunk_pair_cond = jnp.asarray(trunk_pair_cond, dtype=jnp.float32)
253
+ sequence_mask = jnp.asarray(sequence_mask, dtype=jnp.float32)
254
+
255
+ transformer = diffusion_transformer.Transformer(
256
+ self.config.transformer, self.global_config
257
+ )
258
+ act = transformer(
259
+ act=act,
260
+ single_cond=trunk_single_cond,
261
+ mask=sequence_mask,
262
+ pair_cond=trunk_pair_cond,
263
+ )
264
+ act = hm.LayerNorm(
265
+ use_fast_variance=False, create_offset=False, name='output_norm'
266
+ )(act)
267
+ # (n_tokens, per_token_channels)
268
+
269
+ # (Possibly) atom-granularity decoder
270
+ assert isinstance(enc, atom_cross_attention.AtomCrossAttEncoderOutput)
271
+ position_update = atom_cross_attention.atom_cross_att_decoder(
272
+ token_act=act,
273
+ enc=enc,
274
+ config=self.config,
275
+ global_config=self.global_config,
276
+ batch=batch,
277
+ name='diffusion',
278
+ )
279
+
280
+ skip_scaling = SIGMA_DATA**2 / (noise_level**2 + SIGMA_DATA**2)
281
+ out_scaling = (
282
+ noise_level * SIGMA_DATA / jnp.sqrt(noise_level**2 + SIGMA_DATA**2)
283
+ )
284
+ # End `with utils.bfloat16_context()`.
285
+
286
+ return (
287
+ skip_scaling * positions_noisy + out_scaling * position_update
288
+ ) * atom_mask[..., None]
289
+
290
+
291
+ def sample(
292
+ denoising_step: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray],
293
+ batch: feat_batch.Batch,
294
+ key: jnp.ndarray,
295
+ config: SampleConfig,
296
+ ) -> dict[str, jnp.ndarray]:
297
+ """Sample using denoiser on batch.
298
+
299
+ Args:
300
+ denoising_step: the denoising function.
301
+ batch: the batch
302
+ key: random key
303
+ config: config for the sampling process (e.g. number of denoising steps,
304
+ etc.)
305
+
306
+ Returns:
307
+ a dict
308
+ {
309
+ 'atom_positions': jnp.array(...) # shape (<common_axes>, 3)
310
+ 'mask': jnp.array(...) # shape (<common_axes>,)
311
+ }
312
+ where the <common_axes> are
313
+ (num_samples, num_tokens, max_atoms_per_token)
314
+ """
315
+
316
+ mask = batch.predicted_structure_info.atom_mask
317
+
318
+ def apply_denoising_step(carry, noise_level):
319
+ key, positions, noise_level_prev = carry
320
+ key, key_noise, key_aug = jax.random.split(key, 3)
321
+
322
+ positions = random_augmentation(
323
+ rng_key=key_aug, positions=positions, mask=mask
324
+ )
325
+
326
+ gamma = config.gamma_0 * (noise_level > config.gamma_min)
327
+ t_hat = noise_level_prev * (1 + gamma)
328
+
329
+ noise_scale = config.noise_scale * jnp.sqrt(t_hat**2 - noise_level_prev**2)
330
+ noise = noise_scale * jax.random.normal(key_noise, positions.shape)
331
+ positions_noisy = positions + noise
332
+
333
+ positions_denoised = denoising_step(positions_noisy, t_hat)
334
+ grad = (positions_noisy - positions_denoised) / t_hat
335
+
336
+ d_t = noise_level - t_hat
337
+ positions_out = positions_noisy + config.step_scale * d_t * grad
338
+
339
+ return (key, positions_out, noise_level), positions_out
340
+
341
+ num_samples = config.num_samples
342
+
343
+ noise_levels = noise_schedule(jnp.linspace(0, 1, config.steps + 1))
344
+
345
+ key, noise_key = jax.random.split(key)
346
+ positions = jax.random.normal(noise_key, (num_samples,) + mask.shape + (3,))
347
+ positions *= noise_levels[0]
348
+
349
+ init = (
350
+ jax.random.split(key, num_samples),
351
+ positions,
352
+ jnp.tile(noise_levels[None, 0], (num_samples,)),
353
+ )
354
+
355
+ apply_denoising_step = hk.vmap(
356
+ apply_denoising_step, in_axes=(0, None), split_rng=(not hk.running_init())
357
+ )
358
+ result, _ = hk.scan(apply_denoising_step, init, noise_levels[1:], unroll=4)
359
+ _, positions_out, _ = result
360
+
361
+ final_dense_atom_mask = jnp.tile(mask[None], (num_samples, 1, 1))
362
+
363
+ return {'atom_positions': positions_out, 'mask': final_dense_atom_mask}
flax_model/alphafold3/model/network/diffusion_transformer.py ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Diffusion transformer model."""
4
+
5
+ from flax_model.alphafold3.common import base_config
6
+ from flax_model.alphafold3.jax.gated_linear_unit import gated_linear_unit
7
+ from flax_model.alphafold3.model import model_config
8
+ from flax_model.alphafold3.model.atom_layout import atom_layout
9
+ from flax_model.alphafold3.model.components import haiku_modules as hm
10
+ import haiku as hk
11
+ import jax
12
+ from jax import numpy as jnp
13
+
14
+
15
+ def adaptive_layernorm(x, single_cond, name):
16
+ """Adaptive LayerNorm."""
17
+ # Adopted from Scalable Diffusion Models with Transformers
18
+ # https://arxiv.org/abs/2212.09748
19
+ if single_cond is None:
20
+ x = hm.LayerNorm(name=f'{name}layer_norm', use_fast_variance=False)(x)
21
+ else:
22
+ x = hm.LayerNorm(
23
+ name=f'{name}layer_norm',
24
+ use_fast_variance=False,
25
+ create_scale=False,
26
+ create_offset=False,
27
+ )(x)
28
+ single_cond = hm.LayerNorm(
29
+ name=f'{name}single_cond_layer_norm',
30
+ use_fast_variance=False,
31
+ create_offset=False,
32
+ )(single_cond)
33
+ single_scale = hm.Linear(
34
+ x.shape[-1],
35
+ initializer='zeros',
36
+ use_bias=True,
37
+ name=f'{name}single_cond_scale',
38
+ )(single_cond)
39
+ single_bias = hm.Linear(
40
+ x.shape[-1], initializer='zeros', name=f'{name}single_cond_bias'
41
+ )(single_cond)
42
+ x = jax.nn.sigmoid(single_scale) * x + single_bias
43
+ return x
44
+
45
+
46
+ def adaptive_zero_init(
47
+ x, num_channels, single_cond, global_config: model_config.GlobalConfig, name
48
+ ):
49
+ """Adaptive zero init, from AdaLN-zero."""
50
+ if single_cond is None:
51
+ output = hm.Linear(
52
+ num_channels,
53
+ initializer=global_config.final_init,
54
+ name=f'{name}transition2',
55
+ )(x)
56
+ else:
57
+ output = hm.Linear(num_channels, name=f'{name}transition2')(x)
58
+ # Init to a small gain, sigmoid(-2) ~ 0.1
59
+ cond = hm.Linear(
60
+ output.shape[-1],
61
+ initializer='zeros',
62
+ use_bias=True,
63
+ bias_init=-2.0,
64
+ name=f'{name}adaptive_zero_cond',
65
+ )(single_cond)
66
+ output = jax.nn.sigmoid(cond) * output
67
+ return output
68
+
69
+
70
+ def transition_block(
71
+ x: jnp.ndarray,
72
+ num_intermediate_factor: int,
73
+ global_config: model_config.GlobalConfig,
74
+ single_cond: jnp.ndarray | None = None,
75
+ use_glu_kernel: bool = True,
76
+ name: str = '',
77
+ ) -> jnp.ndarray:
78
+ """Transition Block."""
79
+ num_channels = x.shape[-1]
80
+ num_intermediates = num_intermediate_factor * num_channels
81
+
82
+ x = adaptive_layernorm(x, single_cond, name=f'{name}ffw_')
83
+
84
+ if use_glu_kernel:
85
+ weights, _ = hm.haiku_linear_get_params(
86
+ x,
87
+ num_output=num_intermediates * 2,
88
+ initializer='relu',
89
+ name=f'{name}ffw_transition1',
90
+ )
91
+ weights = jnp.reshape(weights, (len(weights), 2, num_intermediates))
92
+ c = gated_linear_unit.gated_linear_unit(
93
+ x=x, weight=weights, implementation=None, activation=jax.nn.swish
94
+ )
95
+ else:
96
+ x = hm.Linear(
97
+ num_intermediates * 2, initializer='relu', name=f'{name}ffw_transition1'
98
+ )(x)
99
+ a, b = jnp.split(x, 2, axis=-1)
100
+ c = jax.nn.swish(a) * b
101
+
102
+ output = adaptive_zero_init(
103
+ c, num_channels, single_cond, global_config, f'{name}ffw_'
104
+ )
105
+ return output
106
+
107
+
108
+ class SelfAttentionConfig(base_config.BaseConfig):
109
+ num_head: int = 16
110
+ key_dim: int | None = None
111
+ value_dim: int | None = None
112
+
113
+
114
+ def self_attention(
115
+ x: jnp.ndarray, # (num_tokens, ch)
116
+ mask: jnp.ndarray, # (num_tokens,)
117
+ pair_logits: jnp.ndarray | None, # (num_heads, num_tokens, num_tokens)
118
+ config: SelfAttentionConfig,
119
+ global_config: model_config.GlobalConfig,
120
+ single_cond: jnp.ndarray | None = None, # (num_tokens, ch)
121
+ name: str = '',
122
+ ) -> jnp.ndarray:
123
+ """Multihead self-attention."""
124
+ assert len(mask.shape) == len(x.shape) - 1, f'{mask.shape}, {x.shape}'
125
+ # bias: ... x heads (1) x query (1) x key
126
+ bias = (1e9 * (mask - 1.0))[..., None, None, :]
127
+
128
+ x = adaptive_layernorm(x, single_cond, name=name)
129
+
130
+ num_channels = x.shape[-1]
131
+ # Sensible default for when the config keys are missing
132
+ key_dim = config.key_dim if config.key_dim is not None else num_channels
133
+ value_dim = config.value_dim if config.value_dim is not None else num_channels
134
+ num_head = config.num_head
135
+ assert key_dim % num_head == 0, f'{key_dim=} % {num_head=} != 0'
136
+ assert value_dim % num_head == 0, f'{value_dim=} % {num_head=} != 0'
137
+ key_dim = key_dim // num_head
138
+ value_dim = value_dim // num_head
139
+
140
+ qk_shape = (num_head, key_dim)
141
+ q = hm.Linear(qk_shape, use_bias=True, name=f'{name}q_projection')(x)
142
+ k = hm.Linear(qk_shape, use_bias=False, name=f'{name}k_projection')(x)
143
+
144
+ # In some situations the gradient norms can blow up without running this
145
+ # einsum in float32.
146
+ q = q.astype(jnp.float32)
147
+ k = k.astype(jnp.float32)
148
+ bias = bias.astype(jnp.float32)
149
+ logits = jnp.einsum('...qhc,...khc->...hqk', q * key_dim ** (-0.5), k) + bias
150
+ if pair_logits is not None:
151
+ logits += pair_logits # (num_heads, seq_len, seq_len)
152
+ weights = jax.nn.softmax(logits, axis=-1)
153
+ weights = jnp.asarray(weights, dtype=x.dtype)
154
+
155
+ v_shape = (num_head, value_dim)
156
+ v = hm.Linear(v_shape, use_bias=False, name=f'{name}v_projection')(x)
157
+ weighted_avg = jnp.einsum('...hqk,...khc->...qhc', weights, v)
158
+ weighted_avg = jnp.reshape(weighted_avg, weighted_avg.shape[:-2] + (-1,))
159
+
160
+ gate_logits = hm.Linear(
161
+ num_head * value_dim,
162
+ bias_init=1.0,
163
+ initializer='zeros',
164
+ name=f'{name}gating_query',
165
+ )(x)
166
+ weighted_avg *= jax.nn.sigmoid(gate_logits)
167
+
168
+ output = adaptive_zero_init(
169
+ weighted_avg, num_channels, single_cond, global_config, name
170
+ )
171
+ return output
172
+
173
+
174
+ class Transformer(hk.Module):
175
+ """Simple transformer stack."""
176
+
177
+ class Config(base_config.BaseConfig):
178
+ attention: SelfAttentionConfig = base_config.autocreate()
179
+ num_blocks: int = 24
180
+ block_remat: bool = False
181
+ super_block_size: int = 4
182
+ num_intermediate_factor: int = 2
183
+
184
+ def __init__(
185
+ self,
186
+ config: Config,
187
+ global_config: model_config.GlobalConfig,
188
+ name: str = 'transformer',
189
+ ):
190
+ super().__init__(name=name)
191
+ self.config = config
192
+ self.global_config = global_config
193
+
194
+ def __call__(
195
+ self,
196
+ act: jnp.ndarray,
197
+ mask: jnp.ndarray,
198
+ single_cond: jnp.ndarray,
199
+ pair_cond: jnp.ndarray | None,
200
+ ) -> jnp.ndarray:
201
+ def block(act, pair_logits):
202
+ act += self_attention(
203
+ act,
204
+ mask,
205
+ pair_logits,
206
+ self.config.attention,
207
+ self.global_config,
208
+ single_cond,
209
+ name=self.name,
210
+ )
211
+ act += transition_block(
212
+ act,
213
+ self.config.num_intermediate_factor,
214
+ self.global_config,
215
+ single_cond,
216
+ name=self.name,
217
+ )
218
+ return act, None
219
+
220
+ # Precompute pair logits for performance
221
+ if pair_cond is None:
222
+ pair_act = None
223
+ else:
224
+ pair_act = hm.LayerNorm(
225
+ name='pair_input_layer_norm',
226
+ use_fast_variance=False,
227
+ create_offset=False,
228
+ )(pair_cond)
229
+
230
+ assert self.config.num_blocks % self.config.super_block_size == 0
231
+ num_super_blocks = self.config.num_blocks // self.config.super_block_size
232
+
233
+ def super_block(act):
234
+ if pair_act is None:
235
+ pair_logits = None
236
+ else:
237
+ pair_logits = hm.Linear(
238
+ (self.config.super_block_size, self.config.attention.num_head),
239
+ name='pair_logits_projection',
240
+ )(pair_act)
241
+ pair_logits = jnp.transpose(pair_logits, [2, 3, 0, 1])
242
+ return hk.experimental.layer_stack(
243
+ self.config.super_block_size, with_per_layer_inputs=True
244
+ )(block)(act, pair_logits)
245
+
246
+ return hk.experimental.layer_stack(
247
+ num_super_blocks, with_per_layer_inputs=True
248
+ )(super_block)(act)[0]
249
+
250
+
251
+ class CrossAttentionConfig(base_config.BaseConfig):
252
+ num_head: int = 4
253
+ key_dim: int = 128
254
+ value_dim: int = 128
255
+
256
+
257
+ def cross_attention(
258
+ x_q: jnp.ndarray, # (..., Q, C)
259
+ x_k: jnp.ndarray, # (..., K, C)
260
+ mask_q: jnp.ndarray, # (..., Q)
261
+ mask_k: jnp.ndarray, # (..., K)
262
+ config: CrossAttentionConfig,
263
+ global_config: model_config.GlobalConfig,
264
+ pair_logits: jnp.ndarray | None = None, # (..., Q, K)
265
+ single_cond_q: jnp.ndarray | None = None, # (..., Q, C)
266
+ single_cond_k: jnp.ndarray | None = None, # (..., K, C)
267
+ name: str = '',
268
+ ) -> jnp.ndarray:
269
+ """Multihead self-attention."""
270
+ assert len(mask_q.shape) == len(x_q.shape) - 1, f'{mask_q.shape}, {x_q.shape}'
271
+ assert len(mask_k.shape) == len(x_k.shape) - 1, f'{mask_k.shape}, {x_k.shape}'
272
+ # bias: ... x heads (1) x query x key
273
+ bias = (
274
+ 1e9
275
+ * (mask_q - 1.0)[..., None, :, None]
276
+ * (mask_k - 1.0)[..., None, None, :]
277
+ )
278
+
279
+ x_q = adaptive_layernorm(x_q, single_cond_q, name=f'{name}q')
280
+ x_k = adaptive_layernorm(x_k, single_cond_k, name=f'{name}k')
281
+
282
+ assert config.key_dim % config.num_head == 0
283
+ assert config.value_dim % config.num_head == 0
284
+ key_dim = config.key_dim // config.num_head
285
+ value_dim = config.value_dim // config.num_head
286
+
287
+ q = hm.Linear(
288
+ (config.num_head, key_dim), use_bias=True, name=f'{name}q_projection'
289
+ )(x_q)
290
+ k = hm.Linear(
291
+ (config.num_head, key_dim), use_bias=False, name=f'{name}k_projection'
292
+ )(x_k)
293
+
294
+ # In some situations the gradient norms can blow up without running this
295
+ # einsum in float32.
296
+ q = q.astype(jnp.float32)
297
+ k = k.astype(jnp.float32)
298
+ bias = bias.astype(jnp.float32)
299
+ logits = jnp.einsum('...qhc,...khc->...hqk', q * key_dim ** (-0.5), k) + bias
300
+ if pair_logits is not None:
301
+ logits += pair_logits
302
+ weights = jax.nn.softmax(logits, axis=-1)
303
+ weights = jnp.asarray(weights, dtype=x_q.dtype)
304
+
305
+ v = hm.Linear(
306
+ (config.num_head, value_dim), use_bias=False, name=f'{name}v_projection'
307
+ )(x_k)
308
+ weighted_avg = jnp.einsum('...hqk,...khc->...qhc', weights, v)
309
+ weighted_avg = jnp.reshape(weighted_avg, weighted_avg.shape[:-2] + (-1,))
310
+
311
+ gate_logits = hm.Linear(
312
+ config.num_head * value_dim,
313
+ bias_init=1.0,
314
+ initializer='zeros',
315
+ name=f'{name}gating_query',
316
+ )(x_q)
317
+ weighted_avg *= jax.nn.sigmoid(gate_logits)
318
+
319
+ output = adaptive_zero_init(
320
+ weighted_avg, x_q.shape[-1], single_cond_q, global_config, name
321
+ )
322
+ return output
323
+
324
+
325
+ class CrossAttTransformer(hk.Module):
326
+ """Transformer that applies cross attention between two sets of subsets."""
327
+
328
+ class Config(base_config.BaseConfig):
329
+ num_intermediate_factor: int
330
+ num_blocks: int
331
+ attention: CrossAttentionConfig = base_config.autocreate()
332
+
333
+ def __init__(
334
+ self,
335
+ config: Config,
336
+ global_config: model_config.GlobalConfig,
337
+ name: str = 'transformer',
338
+ ):
339
+ super().__init__(name=name)
340
+ self.config = config
341
+ self.global_config = global_config
342
+
343
+ def __call__(
344
+ self,
345
+ queries_act: jnp.ndarray, # (num_subsets, num_queries, ch)
346
+ queries_mask: jnp.ndarray, # (num_subsets, num_queries)
347
+ queries_to_keys: atom_layout.GatherInfo, # (num_subsets, num_keys)
348
+ keys_mask: jnp.ndarray, # (num_subsets, num_keys)
349
+ queries_single_cond: jnp.ndarray, # (num_subsets, num_queries, ch)
350
+ keys_single_cond: jnp.ndarray, # (num_subsets, num_keys, ch)
351
+ pair_cond: jnp.ndarray, # (num_subsets, num_queries, num_keys, ch)
352
+ ) -> jnp.ndarray:
353
+ def block(queries_act, pair_logits):
354
+ # copy the queries activations to the keys layout
355
+ keys_act = atom_layout.convert(
356
+ queries_to_keys, queries_act, layout_axes=(-3, -2)
357
+ )
358
+ # cross attention
359
+ queries_act += cross_attention(
360
+ x_q=queries_act,
361
+ x_k=keys_act,
362
+ mask_q=queries_mask,
363
+ mask_k=keys_mask,
364
+ config=self.config.attention,
365
+ global_config=self.global_config,
366
+ pair_logits=pair_logits,
367
+ single_cond_q=queries_single_cond,
368
+ single_cond_k=keys_single_cond,
369
+ name=self.name,
370
+ )
371
+ queries_act += transition_block(
372
+ queries_act,
373
+ self.config.num_intermediate_factor,
374
+ self.global_config,
375
+ queries_single_cond,
376
+ name=self.name,
377
+ )
378
+ return queries_act, None
379
+
380
+ # Precompute pair logits for performance
381
+ pair_act = hm.LayerNorm(
382
+ name='pair_input_layer_norm',
383
+ use_fast_variance=False,
384
+ create_offset=False,
385
+ )(pair_cond)
386
+ # (num_subsets, num_queries, num_keys, num_blocks, num_heads)
387
+ pair_logits = hm.Linear(
388
+ (self.config.num_blocks, self.config.attention.num_head),
389
+ name='pair_logits_projection',
390
+ )(pair_act)
391
+ # (num_block, num_subsets, num_heads, num_queries, num_keys)
392
+ pair_logits = jnp.transpose(pair_logits, [3, 0, 4, 1, 2])
393
+
394
+ return hk.experimental.layer_stack(
395
+ self.config.num_blocks, with_per_layer_inputs=True
396
+ )(block)(queries_act, pair_logits)[0]
flax_model/alphafold3/model/network/distogram_head.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Distogram head."""
4
+
5
+ from typing import Final
6
+
7
+ from flax_model.alphafold3.common import base_config
8
+ from flax_model.alphafold3.model import feat_batch
9
+ from flax_model.alphafold3.model import model_config
10
+ from flax_model.alphafold3.model.components import haiku_modules as hm
11
+ import haiku as hk
12
+ import jax
13
+ import jax.numpy as jnp
14
+
15
+
16
+ _CONTACT_THRESHOLD: Final[float] = 8.0
17
+ _CONTACT_EPSILON: Final[float] = 1e-3
18
+
19
+
20
+ class DistogramHead(hk.Module):
21
+ """Distogram head."""
22
+
23
+ class Config(base_config.BaseConfig):
24
+ first_break: float = 2.3125
25
+ last_break: float = 21.6875
26
+ num_bins: int = 64
27
+
28
+ def __init__(
29
+ self,
30
+ config: Config,
31
+ global_config: model_config.GlobalConfig,
32
+ name='distogram_head',
33
+ ):
34
+ super().__init__(name=name)
35
+ self.config = config
36
+ self.global_config = global_config
37
+
38
+ def __call__(
39
+ self,
40
+ batch: feat_batch.Batch,
41
+ embeddings: dict[str, jnp.ndarray],
42
+ return_distogram: bool = False,
43
+ ) -> dict[str, jnp.ndarray]:
44
+ pair_act = embeddings['pair']
45
+ seq_mask = batch.token_features.mask.astype(bool)
46
+ pair_mask = seq_mask[:, None] * seq_mask[None, :]
47
+
48
+ left_half_logits = hm.Linear(
49
+ self.config.num_bins,
50
+ initializer=self.global_config.final_init,
51
+ name='half_logits',
52
+ )(pair_act)
53
+
54
+ right_half_logits = left_half_logits
55
+ logits = left_half_logits + jnp.swapaxes(right_half_logits, -2, -3)
56
+ probs = jax.nn.softmax(logits, axis=-1)
57
+ breaks = jnp.linspace(
58
+ self.config.first_break,
59
+ self.config.last_break,
60
+ self.config.num_bins - 1,
61
+ )
62
+
63
+ bin_tops = jnp.append(breaks, breaks[-1] + (breaks[-1] - breaks[-2]))
64
+ threshold = _CONTACT_THRESHOLD + _CONTACT_EPSILON
65
+ is_contact_bin = 1.0 * (bin_tops <= threshold)
66
+ contact_probs = jnp.einsum(
67
+ 'ijk,k->ij', probs, is_contact_bin, precision=jax.lax.Precision.HIGHEST
68
+ )
69
+ contact_probs = pair_mask * contact_probs
70
+
71
+ return_dict = {'bin_edges': breaks, 'contact_probs': contact_probs}
72
+ if return_distogram:
73
+ return_dict['distogram'] = logits
74
+
75
+ return return_dict
flax_model/alphafold3/model/network/featurization.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Model-side of the input features processing."""
4
+
5
+ import functools
6
+
7
+ from flax_model.alphafold3.constants import residue_names
8
+ from flax_model.alphafold3.model import feat_batch
9
+ from flax_model.alphafold3.model import features
10
+ from flax_model.alphafold3.model.components import utils
11
+ #import chex
12
+ import jax
13
+ import jax.numpy as jnp
14
+
15
+
16
+ def _grid_keys(key, shape):
17
+ """Generate a grid of rng keys that is consistent with different padding.
18
+
19
+ Generate random keys such that the keys will be identical, regardless of
20
+ how much padding is added to any dimension.
21
+
22
+ Args:
23
+ key: A PRNG key.
24
+ shape: The shape of the output array of keys that will be generated.
25
+
26
+ Returns:
27
+ An array of shape `shape` consisting of random keys.
28
+ """
29
+ if not shape:
30
+ return key
31
+ new_keys = jax.vmap(functools.partial(jax.random.fold_in, key))(
32
+ jnp.arange(shape[0])
33
+ )
34
+ return jax.vmap(functools.partial(_grid_keys, shape=shape[1:]))(new_keys)
35
+
36
+
37
+ def _padding_consistent_rng(f):
38
+ """Modify any element-wise random function to be consistent with padding.
39
+
40
+ Normally if you take a function like jax.random.normal and generate an array,
41
+ say of size (10,10), you will get a different set of random numbers to if you
42
+ add padding and take the first (10,10) sub-array.
43
+
44
+ This function makes a random function that is consistent regardless of the
45
+ amount of padding added.
46
+
47
+ Note: The padding-consistent function is likely to be slower to compile and
48
+ run than the function it is wrapping, but these slowdowns are likely to be
49
+ negligible in a large network.
50
+
51
+ Args:
52
+ f: Any element-wise function that takes (PRNG key, shape) as the first 2
53
+ arguments.
54
+
55
+ Returns:
56
+ An equivalent function to f, that is now consistent for different amounts of
57
+ padding.
58
+ """
59
+
60
+ def inner(key, shape, **kwargs):
61
+ keys = _grid_keys(key, shape)
62
+ signature = (
63
+ '()->()'
64
+ if jax.dtypes.issubdtype(keys.dtype, jax.dtypes.prng_key)
65
+ else '(2)->()'
66
+ )
67
+ return jnp.vectorize(
68
+ functools.partial(f, shape=(), **kwargs), signature=signature
69
+ )(keys)
70
+
71
+ return inner
72
+
73
+
74
+ def gumbel_argsort_sample_idx(
75
+ key: jnp.ndarray, logits: jnp.ndarray
76
+ ) -> jnp.ndarray:
77
+ """Samples with replacement from a distribution given by 'logits'.
78
+
79
+ This uses Gumbel trick to implement the sampling an efficient manner. For a
80
+ distribution over k items this samples k times without replacement, so this
81
+ is effectively sampling a random permutation with probabilities over the
82
+ permutations derived from the logprobs.
83
+
84
+ Args:
85
+ key: prng key
86
+ logits: logarithm of probabilities to sample from, probabilities can be
87
+ unnormalized.
88
+
89
+ Returns:
90
+ Sample from logprobs in one-hot form.
91
+ """
92
+ gumbel = _padding_consistent_rng(jax.random.gumbel)
93
+ z = gumbel(key, logits.shape)
94
+ # This construction is equivalent to jnp.argsort, but using a non stable sort,
95
+ # since stable sort's aren't supported by jax2tf
96
+ axis = len(logits.shape) - 1
97
+ iota = jax.lax.broadcasted_iota(jnp.int64, logits.shape, axis)
98
+ _, perm = jax.lax.sort_key_val(
99
+ logits + z, iota, dimension=-1, is_stable=False
100
+ )
101
+ return perm[::-1]
102
+
103
+
104
+ def create_msa_feat(msa: features.MSA) -> jax.Array:
105
+ """Create and concatenate MSA features."""
106
+ msa_1hot = jax.nn.one_hot(
107
+ msa.rows, residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP + 1
108
+ )
109
+ deletion_matrix = msa.deletion_matrix
110
+ has_deletion = jnp.clip(deletion_matrix, 0.0, 1.0)[..., None]
111
+ deletion_value = (jnp.arctan(deletion_matrix / 3.0) * (2.0 / jnp.pi))[
112
+ ..., None
113
+ ]
114
+
115
+ msa_feat = [
116
+ msa_1hot,
117
+ has_deletion,
118
+ deletion_value,
119
+ ]
120
+
121
+ return jnp.concatenate(msa_feat, axis=-1)
122
+
123
+
124
+ def truncate_msa_batch(msa: features.MSA, num_msa: int) -> features.MSA:
125
+ indices = jnp.arange(num_msa)
126
+ return msa.index_msa_rows(indices)
127
+
128
+
129
+ def create_target_feat(
130
+ batch: feat_batch.Batch,
131
+ append_per_atom_features: bool,
132
+ ) -> jax.Array:
133
+ """Make target feat."""
134
+ token_features = batch.token_features
135
+ target_features = []
136
+ target_features.append(
137
+ jax.nn.one_hot(
138
+ token_features.aatype,
139
+ residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP,
140
+ )
141
+ )
142
+ target_features.append(batch.msa.profile)
143
+ target_features.append(batch.msa.deletion_mean[..., None])
144
+
145
+ # Reference structure features
146
+ if append_per_atom_features:
147
+ ref_mask = batch.ref_structure.mask
148
+ element_feat = jax.nn.one_hot(batch.ref_structure.element, 128)
149
+ element_feat = utils.mask_mean(
150
+ mask=ref_mask[..., None], value=element_feat, axis=-2, eps=1e-6
151
+ )
152
+ target_features.append(element_feat)
153
+ pos_feat = batch.ref_structure.positions
154
+ pos_feat = pos_feat.reshape([pos_feat.shape[0], -1])
155
+ target_features.append(pos_feat)
156
+ target_features.append(ref_mask)
157
+
158
+ return jnp.concatenate(target_features, axis=-1)
159
+
160
+
161
+ def create_relative_encoding(
162
+ seq_features: features.TokenFeatures,
163
+ max_relative_idx: int,
164
+ max_relative_chain: int,
165
+ ) -> jax.Array:
166
+ """Add relative position encodings."""
167
+ rel_feats = []
168
+ token_index = seq_features.token_index
169
+ residue_index = seq_features.residue_index
170
+ asym_id = seq_features.asym_id
171
+ entity_id = seq_features.entity_id
172
+ sym_id = seq_features.sym_id
173
+
174
+ left_asym_id = asym_id[:, None]
175
+ right_asym_id = asym_id[None, :]
176
+
177
+ left_residue_index = residue_index[:, None]
178
+ right_residue_index = residue_index[None, :]
179
+
180
+ left_token_index = token_index[:, None]
181
+ right_token_index = token_index[None, :]
182
+
183
+ left_entity_id = entity_id[:, None]
184
+ right_entity_id = entity_id[None, :]
185
+
186
+ left_sym_id = sym_id[:, None]
187
+ right_sym_id = sym_id[None, :]
188
+
189
+ # Embed relative positions using a one-hot embedding of distance along chain
190
+ offset = left_residue_index - right_residue_index
191
+ clipped_offset = jnp.clip(
192
+ offset + max_relative_idx, min=0, max=2 * max_relative_idx
193
+ )
194
+ asym_id_same = left_asym_id == right_asym_id
195
+ final_offset = jnp.where(
196
+ asym_id_same,
197
+ clipped_offset,
198
+ (2 * max_relative_idx + 1) * jnp.ones_like(clipped_offset),
199
+ )
200
+ rel_pos = jax.nn.one_hot(final_offset, 2 * max_relative_idx + 2)
201
+ rel_feats.append(rel_pos)
202
+
203
+ # Embed relative token index as a one-hot embedding of distance along residue
204
+ token_offset = left_token_index - right_token_index
205
+ clipped_token_offset = jnp.clip(
206
+ token_offset + max_relative_idx, min=0, max=2 * max_relative_idx
207
+ )
208
+ residue_same = (left_asym_id == right_asym_id) & (
209
+ left_residue_index == right_residue_index
210
+ )
211
+ final_token_offset = jnp.where(
212
+ residue_same,
213
+ clipped_token_offset,
214
+ (2 * max_relative_idx + 1) * jnp.ones_like(clipped_token_offset),
215
+ )
216
+ rel_token = jax.nn.one_hot(final_token_offset, 2 * max_relative_idx + 2)
217
+ rel_feats.append(rel_token)
218
+
219
+ # Embed same entity ID
220
+ entity_id_same = left_entity_id == right_entity_id
221
+ rel_feats.append(entity_id_same.astype(rel_pos.dtype)[..., None])
222
+
223
+ # Embed relative chain ID inside each symmetry class
224
+ rel_sym_id = left_sym_id - right_sym_id
225
+
226
+ max_rel_chain = max_relative_chain
227
+
228
+ clipped_rel_chain = jnp.clip(
229
+ rel_sym_id + max_rel_chain, min=0, max=2 * max_rel_chain
230
+ )
231
+
232
+ final_rel_chain = jnp.where(
233
+ entity_id_same,
234
+ clipped_rel_chain,
235
+ (2 * max_rel_chain + 1) * jnp.ones_like(clipped_rel_chain),
236
+ )
237
+ rel_chain = jax.nn.one_hot(final_rel_chain, 2 * max_relative_chain + 2)
238
+
239
+ rel_feats.append(rel_chain)
240
+
241
+ return jnp.concatenate(rel_feats, axis=-1)
242
+
243
+
244
+ def shuffle_msa(
245
+ key: jax.Array, msa: features.MSA
246
+ ) -> tuple[features.MSA, jax.Array]:
247
+ """Shuffle MSA randomly, return batch with shuffled MSA.
248
+
249
+ Args:
250
+ key: rng key for random number generation.
251
+ msa: MSA object to sample msa from.
252
+
253
+ Returns:
254
+ Protein with sampled msa.
255
+ """
256
+ key, sample_key = jax.random.split(key)
257
+ # Sample uniformly among sequences with at least one non-masked position.
258
+ logits = (jnp.clip(jnp.sum(msa.mask, axis=-1), 0.0, 1.0) - 1.0) * 1e6
259
+ index_order = gumbel_argsort_sample_idx(sample_key, logits)
260
+
261
+ return msa.index_msa_rows(index_order), key
flax_model/alphafold3/model/network/noise_level_embeddings.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Fourier embeddings for given noise levels.
4
+
5
+ We supply fixed weights and biases for the Fourier embeddings. These were
6
+ initially generated by the following code, but we make them into constants
7
+ to future proof against changes in jax rng generation:
8
+
9
+ ```
10
+ dim = 256
11
+ w_key, b_key = jax.random.split(jax.random.PRNGKey(42))
12
+ weight = jax.random.normal(w_key, shape=[dim])
13
+ bias = jax.random.uniform(b_key, shape=[dim])
14
+ ```
15
+ """
16
+
17
+ import jax.numpy as jnp
18
+
19
+ # pyformat: disable
20
+ # pylint: disable=bad-whitespace
21
+ # pylint: disable=bad-continuation
22
+ _WEIGHT = [
23
+ 0.45873642, 0.06516238, -0.07278306, -0.26992258, 0.64292115,
24
+ -0.40763968, 3.60116863, 0.54461384, -0.32644904, 2.10888267,
25
+ 1.30805349, 1.19838560, -1.37745857, 1.99475312, -1.64120293,
26
+ 1.07823789, -0.02288206, 0.88305283, 0.48099944, 0.17655374,
27
+ 0.30281949, 0.80646873, 0.62605333, -0.23965347, -1.02609432,
28
+ 0.75006109, -0.19913037, 0.07466396, 0.66431236, -0.60990530,
29
+ -0.69709194, -0.44453633, -1.77656078, 0.02299878, 0.04095552,
30
+ 0.35485864, -0.47602659, -0.98820388, -0.24106771, -1.07254291,
31
+ -0.99741757, 0.22697604, 1.41390419, 1.54984057, -0.12237291,
32
+ 0.20156337, 0.61767143, 0.23959029, 0.92454034, 1.84082258,
33
+ 0.89030224, 0.39598912, -1.52224910, 0.29669049, 1.52356744,
34
+ -0.33968377, 0.24155144, -0.52308381, -0.23622665, 0.92825454,
35
+ -0.63864607, -0.62169307, 0.78623551, -0.80352145, -0.45496067,
36
+ 1.30877995, -0.06686528, 1.00248849, -0.63593471, 0.16372502,
37
+ -1.46133232, 1.10562658, -0.01693927, 0.28684548, -0.72843230,
38
+ 0.66133535, -1.92225552, 0.70241231, -0.96868867, -0.47309339,
39
+ -1.66894221, 0.46018723, -0.56806105, 0.32694784, -0.46529883,
40
+ 1.02299964, 0.84688205, 1.19581807, -1.82454145, 0.05999713,
41
+ -0.59530073, 1.44862521, -0.34933713, -0.46564487, -0.55005538,
42
+ -1.61170268, 0.17502306, 0.38670063, -1.12133658, -0.29343036,
43
+ -0.52527446, -1.26285112, 1.07982683, 0.51215219, 1.48963666,
44
+ 1.09847653, -0.01563358, 0.32574457, 1.94779706, -1.29198587,
45
+ 1.06249654, -0.86965990, 0.22975266, -0.27182648, -0.21130897,
46
+ -0.41773933, -0.02329035, 1.31049252, 0.05579265, -1.23127055,
47
+ -0.99691105, 0.27058721, -0.72509319, -0.14421797, -1.48605061,
48
+ 1.35041201, 1.29619241, -1.01022530, -0.79787987, -0.16166858,
49
+ 0.87210685, 1.69248152, 1.42469788, -0.72325104, -1.24823737,
50
+ 0.07051118, 0.71332991, -0.07360429, -0.91955227, -2.68856549,
51
+ -0.44033936, 0.35482934, -0.57933813, 0.97468042, -0.31050494,
52
+ -0.88454425, -2.08785224, 0.47322822, -0.02400172, 0.26644820,
53
+ -0.19147627, -2.10538960, -1.27962470, -1.35999286, 2.09867334,
54
+ 0.65099514, 0.21604492, -0.45951018, 0.15994427, -0.31420693,
55
+ -0.65202618, -0.61077976, -1.06100249, -1.47254968, 1.18165290,
56
+ -0.78656220, 1.28182006, 1.80323684, 1.09196901, 0.26118696,
57
+ -0.30168581, 0.39749333, 0.26812574, -1.51995814, -0.46909946,
58
+ 0.03874255, -1.36774313, 2.30143976, 2.06959820, -0.41647521,
59
+ 1.85624206, 0.49019700, -0.06726539, 0.00457313, 0.23915423,
60
+ -1.84971249, -0.20482327, -0.34097880, -0.57933033, -1.10541213,
61
+ -0.30269983, -0.16430426, -0.82371718, 0.10345812, 1.78753936,
62
+ 0.04786763, 1.86778629, -0.65214992, 0.81544143, -0.28214937,
63
+ 0.31187257, 0.57661986, 1.21938801, -1.56046617, 0.38046429,
64
+ -0.18235965, 0.81794524, -0.40474343, 0.46538028, -1.15558851,
65
+ 0.59625793, -1.07801270, 0.07310858, 0.61526084, 0.55518496,
66
+ -0.49787554, 0.92703879, -1.27780271, -0.83373469, -0.43015575,
67
+ 0.41877759, -1.03987372, -1.46055734, 0.61282396, 0.15590595,
68
+ -0.34269521, 0.56509072, -1.17904210, 0.11374855, -1.83310866,
69
+ 0.38734794, -0.58623004, 0.77931106, 1.53930688, -0.70299625,
70
+ -0.11389336, -1.14818096, -0.44400632, 1.21887410, 0.64066756,
71
+ -0.70249403, -0.27244881, 0.38586098, -1.07925785, 0.12448707,
72
+ -1.28286278, 0.37827531, 0.68812364, 1.65695465, 0.12440517,
73
+ -0.03689830, 1.10224664, -0.28323629, -0.47939169, 0.70120829,
74
+ -0.67204583
75
+ ]
76
+
77
+ _BIAS = [
78
+ 0.00465965, 0.21738243, 0.22277749, 0.68463874, 0.84596848, 0.17337036,
79
+ 0.39573753, 0.78153563, 0.86311185, 0.21782327, 0.24377882, 0.42310703,
80
+ 0.19887352, 0.10486019, 0.48707581, 0.22205460, 0.97263455, 0.29714966,
81
+ 0.11244559, 0.53020525, 0.36796236, 0.37294638, 0.80261672, 0.04669094,
82
+ 0.86319661, 0.75907171, 0.77297020, 0.01114798, 0.55850804, 0.91799915,
83
+ 0.23032320, 0.12154722, 0.26701927, 0.42934716, 0.47951782, 0.96782577,
84
+ 0.86785042, 0.61985648, 0.05743814, 0.41800117, 0.68881893, 0.60575199,
85
+ 0.21058667, 0.64412105, 0.63958526, 0.89390790, 0.69755554, 0.89345169,
86
+ 0.53330755, 0.56985939, 0.30724049, 0.00984561, 0.91407037, 0.92118979,
87
+ 0.94153070, 0.81097460, 0.70537627, 0.32810748, 0.47227263, 0.11821401,
88
+ 0.44983089, 0.30767226, 0.31756389, 0.62969446, 0.69892538, 0.16949117,
89
+ 0.06207097, 0.46717727, 0.95348179, 0.62363589, 0.49018729, 0.06920040,
90
+ 0.39333904, 0.41299903, 0.52514863, 0.61197245, 0.56871891, 0.65053988,
91
+ 0.22203422, 0.46748531, 0.86931503, 0.87050021, 0.40208721, 0.32084906,
92
+ 0.55084610, 0.94584596, 0.76279902, 0.36250532, 0.74272907, 0.66682065,
93
+ 0.96452832, 0.64768302, 0.88070846, 0.56995463, 0.06395614, 0.69499350,
94
+ 0.44494808, 0.39775658, 0.20280898, 0.33363521, 0.05999005, 0.44414878,
95
+ 0.65227020, 0.01199079, 0.71995056, 0.19045687, 0.48342144, 0.25127733,
96
+ 0.66515994, 0.22465158, 0.22313106, 0.06302810, 0.55783665, 0.93625581,
97
+ 0.58800840, 0.72525370, 0.52879298, 0.77195418, 0.15548682, 0.01028740,
98
+ 0.39325142, 0.45401239, 0.71494079, 0.33011997, 0.05050695, 0.26381660,
99
+ 0.63064706, 0.47604024, 0.08593416, 0.00383425, 0.06352687, 0.05510247,
100
+ 0.03552997, 0.35810637, 0.56094289, 0.60922170, 0.88599777, 0.45419788,
101
+ 0.40486634, 0.71297824, 0.34976673, 0.97825217, 0.12915993, 0.09566259,
102
+ 0.64318919, 0.16717327, 0.82308614, 0.32672071, 0.81688786, 0.84857118,
103
+ 0.99922776, 0.07551706, 0.18766022, 0.13051236, 0.39136350, 0.08768725,
104
+ 0.92048228, 0.87185788, 0.39158428, 0.79224777, 0.17492688, 0.68902445,
105
+ 0.81980729, 0.70458186, 0.59489477, 0.93324888, 0.49986637, 0.40705478,
106
+ 0.89202917, 0.20673239, 0.39339757, 0.20996964, 0.02923799, 0.53992438,
107
+ 0.40119815, 0.10366607, 0.08044600, 0.95551598, 0.20518017, 0.68826210,
108
+ 0.90159297, 0.69008791, 0.86880815, 0.16246438, 0.89628279, 0.11481643,
109
+ 0.61353648, 0.41545081, 0.92478311, 0.78212476, 0.48292696, 0.79621077,
110
+ 0.11947489, 0.01747024, 0.22928023, 0.87387264, 0.86349785, 0.89526737,
111
+ 0.58904779, 0.13896775, 0.68194926, 0.55824125, 0.44428205, 0.55422378,
112
+ 0.28189969, 0.27923775, 0.09979951, 0.66994715, 0.45943546, 0.71207762,
113
+ 0.17300689, 0.83434916, 0.02573085, 0.45858085, 0.55934799, 0.30676675,
114
+ 0.52219367, 0.34544575, 0.19280875, 0.26937950, 0.07147646, 0.06295013,
115
+ 0.76382887, 0.38737607, 0.58825982, 0.17423475, 0.05509448, 0.97228825,
116
+ 0.94380617, 0.91664016, 0.18800116, 0.41771865, 0.59420645, 0.77371931,
117
+ 0.64687788, 0.27284670, 0.22310913, 0.15663862, 0.45573199, 0.50386798,
118
+ 0.66712272, 0.71649647, 0.28475654, 0.83415413, 0.75261366, 0.61517799,
119
+ 0.93544555, 0.76141870, 0.85474241, 0.74766934, 0.33459592, 0.78477907,
120
+ 0.07250881, 0.10174239, 0.95332730, 0.80793905
121
+ ]
122
+ # pyformat: enable
123
+ # pylint: enable=bad-whitespace
124
+ # pylint: enable=bad-continuation
125
+
126
+
127
+ def noise_embeddings(sigma_scaled_noise_level: jnp.ndarray) -> jnp.ndarray:
128
+ """Returns Fourier noise level embeddings for diffusion model."""
129
+ transformed_noise_level = (1 / 4) * jnp.log(sigma_scaled_noise_level)
130
+ weight = jnp.array(_WEIGHT, dtype=jnp.float32)
131
+ bias = jnp.array(_BIAS, dtype=jnp.float32)
132
+ embeddings = transformed_noise_level[..., None] * weight + bias
133
+ return jnp.cos(2 * jnp.pi * embeddings)
flax_model/alphafold3/model/network/template_modules.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Modules for embedding templates."""
4
+
5
+ from flax_model.alphafold3.common import base_config
6
+ from flax_model.alphafold3.constants import residue_names
7
+ from flax_model.alphafold3.jax import geometry
8
+ from flax_model.alphafold3.model import features
9
+ from flax_model.alphafold3.model import model_config
10
+ from flax_model.alphafold3.model import protein_data_processing
11
+ from flax_model.alphafold3.model.components import haiku_modules as hm
12
+ from flax_model.alphafold3.model.network import modules
13
+ from flax_model.alphafold3.model.scoring import scoring
14
+ import haiku as hk
15
+ import jax
16
+ import jax.numpy as jnp
17
+
18
+
19
+ class DistogramFeaturesConfig(base_config.BaseConfig):
20
+ # The left edge of the first bin.
21
+ min_bin: float = 3.25
22
+ # The left edge of the final bin. The final bin catches everything larger than
23
+ # `max_bin`.
24
+ max_bin: float = 50.75
25
+ # The number of bins in the distogram.
26
+ num_bins: int = 39
27
+
28
+
29
+ def dgram_from_positions(positions, config: DistogramFeaturesConfig):
30
+ """Compute distogram from amino acid positions.
31
+
32
+ Args:
33
+ positions: (num_res, 3) Position coordinates.
34
+ config: Distogram bin configuration.
35
+
36
+ Returns:
37
+ Distogram with the specified number of bins.
38
+ """
39
+ lower_breaks = jnp.linspace(config.min_bin, config.max_bin, config.num_bins)
40
+ lower_breaks = jnp.square(lower_breaks)
41
+ upper_breaks = jnp.concatenate(
42
+ [lower_breaks[1:], jnp.array([1e8], dtype=jnp.float32)], axis=-1
43
+ )
44
+ dist2 = jnp.sum(
45
+ jnp.square(
46
+ jnp.expand_dims(positions, axis=-2)
47
+ - jnp.expand_dims(positions, axis=-3)
48
+ ),
49
+ axis=-1,
50
+ keepdims=True,
51
+ )
52
+
53
+ dgram = (dist2 > lower_breaks).astype(jnp.float32) * (
54
+ dist2 < upper_breaks
55
+ ).astype(jnp.float32)
56
+ return dgram
57
+
58
+
59
+ def make_backbone_rigid(
60
+ positions: geometry.Vec3Array,
61
+ mask: jnp.ndarray,
62
+ group_indices: jnp.ndarray,
63
+ ) -> tuple[geometry.Rigid3Array, jnp.ndarray]:
64
+ """Make backbone Rigid3Array and mask.
65
+
66
+ Args:
67
+ positions: (num_res, num_atoms) of atom positions as Vec3Array.
68
+ mask: (num_res, num_atoms) for atom mask.
69
+ group_indices: (num_res, num_group, 3) for atom indices forming groups.
70
+
71
+ Returns:
72
+ tuple of backbone Rigid3Array and mask (num_res,).
73
+ """
74
+ backbone_indices = group_indices[:, 0]
75
+
76
+ # main backbone frames differ in sidechain frame convention.
77
+ # for sidechain it's (C, CA, N), for backbone it's (N, CA, C)
78
+ # Hence using c, b, a, each of shape (num_res,).
79
+ c, b, a = [backbone_indices[..., i] for i in range(3)]
80
+
81
+ slice_index = jax.vmap(lambda x, i: x[i])
82
+ rigid_mask = (
83
+ slice_index(mask, a) * slice_index(mask, b) * slice_index(mask, c)
84
+ ).astype(jnp.float32)
85
+
86
+ frame_positions = []
87
+ for indices in [a, b, c]:
88
+ frame_positions.append(
89
+ jax.tree.map(lambda x, idx=indices: slice_index(x, idx), positions)
90
+ )
91
+
92
+ rotation = geometry.Rot3Array.from_two_vectors(
93
+ frame_positions[2] - frame_positions[1],
94
+ frame_positions[0] - frame_positions[1],
95
+ )
96
+ rigid = geometry.Rigid3Array(rotation, frame_positions[1])
97
+
98
+ return rigid, rigid_mask
99
+
100
+
101
+ class TemplateEmbedding(hk.Module):
102
+ """Embed a set of templates."""
103
+
104
+ class Config(base_config.BaseConfig):
105
+ num_channels: int = 64
106
+ template_stack: modules.PairFormerIteration.Config = base_config.autocreate(
107
+ num_layer=2,
108
+ pair_transition=base_config.autocreate(num_intermediate_factor=2),
109
+ )
110
+ dgram_features: DistogramFeaturesConfig = base_config.autocreate()
111
+
112
+ def __init__(
113
+ self,
114
+ config: Config,
115
+ global_config: model_config.GlobalConfig,
116
+ name='template_embedding',
117
+ ):
118
+ super().__init__(name=name)
119
+ self.config = config
120
+ self.global_config = global_config
121
+
122
+ def __call__(
123
+ self,
124
+ query_embedding: jnp.ndarray,
125
+ templates: features.Templates,
126
+ padding_mask_2d: jnp.ndarray,
127
+ multichain_mask_2d: jnp.ndarray,
128
+ key: jnp.ndarray,
129
+ ) -> jnp.ndarray:
130
+ """Generate an embedding for a set of templates.
131
+
132
+ Args:
133
+ query_embedding: [num_res, num_res, num_channel] a query tensor that will
134
+ be used to attend over the templates to remove the num_templates
135
+ dimension.
136
+ templates: A 'Templates' object.
137
+ padding_mask_2d: [num_res, num_res] Pair mask for attention operations.
138
+ multichain_mask_2d: [num_res, num_res] Pair mask for multichain.
139
+ key: random key generator.
140
+
141
+ Returns:
142
+ An embedding of size [num_res, num_res, num_channels]
143
+ """
144
+ c = self.config
145
+ num_residues = query_embedding.shape[0]
146
+ num_templates = templates.aatype.shape[0]
147
+ query_num_channels = query_embedding.shape[2]
148
+ num_atoms = 24
149
+ assert query_embedding.shape == (
150
+ num_residues,
151
+ num_residues,
152
+ query_num_channels,
153
+ )
154
+ assert templates.aatype.shape == (num_templates, num_residues)
155
+ assert templates.atom_positions.shape == (
156
+ num_templates,
157
+ num_residues,
158
+ num_atoms,
159
+ 3,
160
+ )
161
+ assert templates.atom_mask.shape == (num_templates, num_residues, num_atoms)
162
+ assert padding_mask_2d.shape == (num_residues, num_residues)
163
+
164
+ num_templates = templates.aatype.shape[0]
165
+ num_res, _, query_num_channels = query_embedding.shape
166
+
167
+ # Embed each template separately.
168
+ template_embedder = SingleTemplateEmbedding(self.config, self.global_config)
169
+
170
+ subkeys = jnp.array(jax.random.split(key, num_templates))
171
+
172
+ def scan_fn(carry, x):
173
+ templates, key = x
174
+ embedding = template_embedder(
175
+ query_embedding,
176
+ templates,
177
+ padding_mask_2d,
178
+ multichain_mask_2d,
179
+ key,
180
+ )
181
+ return carry + embedding, None
182
+
183
+ scan_init = jnp.zeros(
184
+ (num_res, num_res, c.num_channels), dtype=query_embedding.dtype
185
+ )
186
+ summed_template_embeddings, _ = hk.scan(
187
+ scan_fn, scan_init, (templates, subkeys)
188
+ )
189
+
190
+ embedding = summed_template_embeddings / (1e-7 + num_templates)
191
+ embedding = jax.nn.relu(embedding)
192
+ embedding = hm.Linear(
193
+ query_num_channels, initializer='relu', name='output_linear'
194
+ )(embedding)
195
+
196
+ assert embedding.shape == (num_residues, num_residues, query_num_channels)
197
+ return embedding
198
+
199
+
200
+ class SingleTemplateEmbedding(hk.Module):
201
+ """Embed a single template."""
202
+
203
+ def __init__(
204
+ self,
205
+ config: TemplateEmbedding.Config,
206
+ global_config: model_config.GlobalConfig,
207
+ name='single_template_embedding',
208
+ ):
209
+ super().__init__(name=name)
210
+ self.config = config
211
+ self.global_config = global_config
212
+
213
+ def __call__(
214
+ self,
215
+ query_embedding: jnp.ndarray,
216
+ templates: features.Templates,
217
+ padding_mask_2d: jnp.ndarray,
218
+ multichain_mask_2d: jnp.ndarray,
219
+ key: jnp.ndarray,
220
+ ) -> jnp.ndarray:
221
+ """Build the single template embedding graph.
222
+
223
+ Args:
224
+ query_embedding: (num_res, num_res, num_channels) - embedding of the query
225
+ sequence/msa.
226
+ templates: 'Templates' object containing single Template.
227
+ padding_mask_2d: Padding mask (Note: this doesn't care if a template
228
+ exists, unlike the template_pseudo_beta_mask).
229
+ multichain_mask_2d: A mask indicating intra-chain residue pairs, used to
230
+ mask out between chain distances/features when templates are for single
231
+ chains.
232
+ key: Random key generator.
233
+
234
+ Returns:
235
+ A template embedding (num_res, num_res, num_channels).
236
+ """
237
+ gc = self.global_config
238
+ c = self.config
239
+ assert padding_mask_2d.dtype == query_embedding.dtype
240
+ dtype = query_embedding.dtype
241
+ num_channels = self.config.num_channels
242
+
243
+ def construct_input(
244
+ query_embedding, templates: features.Templates, multichain_mask_2d
245
+ ):
246
+
247
+ # Compute distogram feature for the template.
248
+ aatype = templates.aatype
249
+ dense_atom_mask = templates.atom_mask
250
+
251
+ dense_atom_positions = templates.atom_positions
252
+ dense_atom_positions *= dense_atom_mask[..., None]
253
+
254
+ pseudo_beta_positions, pseudo_beta_mask = scoring.pseudo_beta_fn(
255
+ templates.aatype, dense_atom_positions, dense_atom_mask
256
+ )
257
+ pseudo_beta_mask_2d = (
258
+ pseudo_beta_mask[:, None] * pseudo_beta_mask[None, :]
259
+ )
260
+ pseudo_beta_mask_2d *= multichain_mask_2d
261
+ dgram = dgram_from_positions(
262
+ pseudo_beta_positions, self.config.dgram_features
263
+ )
264
+ dgram *= pseudo_beta_mask_2d[..., None]
265
+ dgram = dgram.astype(dtype)
266
+ pseudo_beta_mask_2d = pseudo_beta_mask_2d.astype(dtype)
267
+ to_concat = [(dgram, 1), (pseudo_beta_mask_2d, 0)]
268
+
269
+ aatype = jax.nn.one_hot(
270
+ aatype,
271
+ residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP,
272
+ axis=-1,
273
+ dtype=dtype,
274
+ )
275
+ to_concat.append((aatype[None, :, :], 1))
276
+ to_concat.append((aatype[:, None, :], 1))
277
+
278
+ # Compute a feature representing the normalized vector between each
279
+ # backbone affine - i.e. in each residues local frame, what direction are
280
+ # each of the other residues.
281
+
282
+ template_group_indices = jnp.take(
283
+ protein_data_processing.RESTYPE_RIGIDGROUP_DENSE_ATOM_IDX,
284
+ templates.aatype,
285
+ axis=0,
286
+ )
287
+ rigid, backbone_mask = make_backbone_rigid(
288
+ geometry.Vec3Array.from_array(dense_atom_positions),
289
+ dense_atom_mask,
290
+ template_group_indices.astype(jnp.int32),
291
+ )
292
+ points = rigid.translation
293
+ rigid_vec = rigid[:, None].inverse().apply_to_point(points)
294
+ unit_vector = rigid_vec.normalized()
295
+ unit_vector = [unit_vector.x, unit_vector.y, unit_vector.z]
296
+
297
+ unit_vector = [x.astype(dtype) for x in unit_vector]
298
+ backbone_mask = backbone_mask.astype(dtype)
299
+
300
+ backbone_mask_2d = backbone_mask[:, None] * backbone_mask[None, :]
301
+ backbone_mask_2d *= multichain_mask_2d
302
+ unit_vector = [x * backbone_mask_2d for x in unit_vector]
303
+
304
+ # Note that the backbone_mask takes into account C, CA and N (unlike
305
+ # pseudo beta mask which just needs CB) so we add both masks as features.
306
+ to_concat.extend([(x, 0) for x in unit_vector])
307
+ to_concat.append((backbone_mask_2d, 0))
308
+
309
+ query_embedding = hm.LayerNorm(name='query_embedding_norm')(
310
+ query_embedding
311
+ )
312
+ # Allow the template embedder to see the query embedding. Note this
313
+ # contains the position relative feature, so this is how the network knows
314
+ # which residues are next to each other.
315
+ to_concat.append((query_embedding, 1))
316
+
317
+ act = 0
318
+
319
+ for i, (x, n_input_dims) in enumerate(to_concat):
320
+ act += hm.Linear(
321
+ num_channels,
322
+ num_input_dims=n_input_dims,
323
+ initializer='relu',
324
+ name=f'template_pair_embedding_{i}',
325
+ )(x)
326
+ return act
327
+
328
+ act = construct_input(query_embedding, templates, multichain_mask_2d)
329
+
330
+ if c.template_stack.num_layer:
331
+
332
+ def template_iteration_fn(x):
333
+ return modules.PairFormerIteration(
334
+ c.template_stack, gc, name='template_embedding_iteration'
335
+ )(act=x, pair_mask=padding_mask_2d)
336
+
337
+ template_stack = hk.experimental.layer_stack(c.template_stack.num_layer)(
338
+ template_iteration_fn
339
+ )
340
+ act = template_stack(act)
341
+
342
+ act = hm.LayerNorm(name='output_layer_norm')(act)
343
+ return act
flax_model/alphafold3/model/params.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Model param loading."""
4
+
5
+ import bisect
6
+ import collections
7
+ from collections.abc import Iterator
8
+ import contextlib
9
+ import io
10
+ import os
11
+ import pathlib
12
+ import re
13
+ import struct
14
+ import sys
15
+ from typing import IO
16
+
17
+ import haiku as hk
18
+ import jax.numpy as jnp
19
+ import numpy as np
20
+ import zstandard
21
+
22
+
23
+ class RecordError(Exception):
24
+ """Error reading a record."""
25
+
26
+
27
+ def encode_record(scope: str, name: str, arr: np.ndarray) -> bytes:
28
+ """Encodes a single haiku param as bytes, preserving non-numpy dtypes."""
29
+ scope = scope.encode('utf-8')
30
+ name = name.encode('utf-8')
31
+ shape = arr.shape
32
+ dtype = str(arr.dtype).encode('utf-8')
33
+ arr = np.ascontiguousarray(arr)
34
+ if sys.byteorder == 'big':
35
+ arr = arr.byteswap()
36
+ arr_buffer = arr.tobytes('C')
37
+ header = struct.pack(
38
+ '<5i', len(scope), len(name), len(dtype), len(shape), len(arr_buffer)
39
+ )
40
+ return header + b''.join(
41
+ (scope, name, dtype, struct.pack(f'{len(shape)}i', *shape), arr_buffer)
42
+ )
43
+
44
+
45
+ def _read_record(stream: IO[bytes]) -> tuple[str, str, np.ndarray] | None:
46
+ """Reads a record encoded by `_encode_record` from a byte stream."""
47
+ header_size = struct.calcsize('<5i')
48
+ header = stream.read(header_size)
49
+ if not header:
50
+ return None
51
+ if len(header) < header_size:
52
+ raise RecordError(f'Incomplete header: {len(header)=} < {header_size=}')
53
+ (scope_len, name_len, dtype_len, shape_len, arr_buffer_len) = struct.unpack(
54
+ '<5i', header
55
+ )
56
+ fmt = f'<{scope_len}s{name_len}s{dtype_len}s{shape_len}i'
57
+ payload_size = struct.calcsize(fmt) + arr_buffer_len
58
+ payload = stream.read(payload_size)
59
+ if len(payload) < payload_size:
60
+ raise RecordError(f'Incomplete payload: {len(payload)=} < {payload_size=}')
61
+ scope, name, dtype, *shape = struct.unpack_from(fmt, payload)
62
+ scope = scope.decode('utf-8')
63
+ name = name.decode('utf-8')
64
+ dtype = dtype.decode('utf-8')
65
+ arr = np.frombuffer(payload[-arr_buffer_len:], dtype=dtype)
66
+ arr = np.reshape(arr, shape)
67
+ if sys.byteorder == 'big':
68
+ arr = arr.byteswap()
69
+ return scope, name, arr
70
+
71
+
72
+ def read_records(stream: IO[bytes]) -> Iterator[tuple[str, str, np.ndarray]]:
73
+ """Fully reads the contents of a byte stream."""
74
+ while record := _read_record(stream):
75
+ yield record
76
+
77
+
78
+ class _MultiFileIO(io.RawIOBase):
79
+ """A file-like object that presents a concatenated view of multiple files."""
80
+
81
+ def __init__(self, files: list[pathlib.Path]):
82
+ self._files = files
83
+ self._stack = contextlib.ExitStack()
84
+ self._handles = [
85
+ self._stack.enter_context(file.open('rb')) for file in files
86
+ ]
87
+ self._sizes = []
88
+ for handle in self._handles:
89
+ handle.seek(0, os.SEEK_END)
90
+ self._sizes.append(handle.tell())
91
+ self._length = sum(self._sizes)
92
+ self._offsets = [0]
93
+ for s in self._sizes[:-1]:
94
+ self._offsets.append(self._offsets[-1] + s)
95
+ self._abspos = 0
96
+ self._relpos = (0, 0)
97
+
98
+ def _abs_to_rel(self, pos: int) -> tuple[int, int]:
99
+ idx = bisect.bisect_right(self._offsets, pos) - 1
100
+ return idx, pos - self._offsets[idx]
101
+
102
+ def close(self):
103
+ self._stack.close()
104
+
105
+ def closed(self) -> bool:
106
+ return all(handle.closed for handle in self._handles)
107
+
108
+ def fileno(self) -> int:
109
+ return -1
110
+
111
+ def readable(self) -> bool:
112
+ return True
113
+
114
+ def tell(self) -> int:
115
+ return self._abspos
116
+
117
+ def seek(self, pos: int, whence: int = os.SEEK_SET, /):
118
+ match whence:
119
+ case os.SEEK_SET:
120
+ pass
121
+ case os.SEEK_CUR:
122
+ pos += self._abspos
123
+ case os.SEEK_END:
124
+ pos = self._length - pos
125
+ case _:
126
+ raise ValueError(f'Invalid whence: {whence}')
127
+ self._abspos = pos
128
+ self._relpos = self._abs_to_rel(pos)
129
+
130
+ def readinto(self, b: bytearray | memoryview) -> int:
131
+ result = 0
132
+ mem = memoryview(b)
133
+ while mem:
134
+ self._handles[self._relpos[0]].seek(self._relpos[1])
135
+ count = self._handles[self._relpos[0]].readinto(mem)
136
+ result += count
137
+ self._abspos += count
138
+ self._relpos = self._abs_to_rel(self._abspos)
139
+ mem = mem[count:]
140
+ if self._abspos == self._length:
141
+ break
142
+ return result
143
+
144
+
145
+ @contextlib.contextmanager
146
+ def open_for_reading(model_files: list[pathlib.Path], is_compressed: bool):
147
+ with contextlib.closing(_MultiFileIO(model_files)) as f:
148
+ if is_compressed:
149
+ yield zstandard.ZstdDecompressor().stream_reader(f)
150
+ else:
151
+ yield f
152
+
153
+
154
+ def _match_model(
155
+ paths: list[pathlib.Path], pattern: re.Pattern[str]
156
+ ) -> dict[str, list[pathlib.Path]]:
157
+ """Match files in a directory with a pattern, and group by model name."""
158
+ models = collections.defaultdict(list)
159
+ for path in paths:
160
+ match = pattern.fullmatch(path.name)
161
+ if match:
162
+ models[match.group('model_name')].append(path)
163
+ return {k: sorted(v) for k, v in models.items()}
164
+
165
+
166
+ def select_model_files(
167
+ model_dir: pathlib.Path, model_name: str | None = None
168
+ ) -> tuple[list[pathlib.Path], bool]:
169
+ """Select the model files from a model directory."""
170
+ files = [file for file in model_dir.iterdir() if file.is_file()]
171
+
172
+ for pattern, is_compressed in (
173
+ (r'(?P<model_name>.*)\.[0-9]+\.bin\.zst$', True),
174
+ (r'(?P<model_name>.*)\.bin\.zst\.[0-9]+$', True),
175
+ (r'(?P<model_name>.*)\.[0-9]+\.bin$', False),
176
+ (r'(?P<model_name>.*)\.bin]\.[0-9]+$', False),
177
+ (r'(?P<model_name>.*)\.bin\.zst$', True),
178
+ (r'(?P<model_name>.*)\.bin$', False),
179
+ ):
180
+ models = _match_model(files, re.compile(pattern))
181
+ if model_name is not None:
182
+ if model_name in models:
183
+ return models[model_name], is_compressed
184
+ else:
185
+ if models:
186
+ if len(models) > 1:
187
+ raise RuntimeError(f'Multiple models matched in {model_dir}')
188
+ _, model_files = models.popitem()
189
+ return model_files, is_compressed
190
+ raise FileNotFoundError(f'No models matched in {model_dir}')
191
+
192
+
193
+ def get_model_haiku_params(model_dir: pathlib.Path) -> hk.Params:
194
+ """Get the Haiku parameters from a model name."""
195
+ params: dict[str, dict[str, jnp.Array]] = {}
196
+ model_files, is_compressed = select_model_files(model_dir)
197
+ with open_for_reading(model_files, is_compressed) as stream:
198
+ for scope, name, arr in read_records(stream):
199
+ params.setdefault(scope, {})[name] = jnp.array(arr)
200
+ if not params:
201
+ raise FileNotFoundError(f'Model missing from "{model_dir}"')
202
+ return params
flax_model/alphafold3/model/pipeline/__init__.py ADDED
File without changes
flax_model/alphafold3/model/pipeline/structure_cleaning.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Prepare PDB structure for training or inference."""
4
+
5
+ from typing import Any
6
+
7
+ from absl import logging
8
+ from flax_model.alphafold3 import structure
9
+ from flax_model.alphafold3.constants import chemical_component_sets
10
+ from flax_model.alphafold3.constants import chemical_components
11
+ from flax_model.alphafold3.constants import mmcif_names
12
+ from flax_model.alphafold3.model.atom_layout import atom_layout
13
+ from flax_model.alphafold3.model.pipeline import inter_chain_bonds
14
+ from flax_model.alphafold3.model.scoring import covalent_bond_cleaning
15
+ from flax_model.alphafold3.structure import sterics
16
+ import numpy as np
17
+
18
+
19
+ def _get_leaving_atom_mask(
20
+ struc: structure.Structure,
21
+ polymer_ligand_bonds: atom_layout.AtomLayout | None,
22
+ ligand_ligand_bonds: atom_layout.AtomLayout | None,
23
+ chain_id: str,
24
+ chain_type: str,
25
+ res_id: int,
26
+ res_name: str,
27
+ ) -> np.ndarray:
28
+ """Updates a drop_leaving_atoms mask with new leaving atom locations."""
29
+ bonded_atoms = atom_layout.get_bonded_atoms(
30
+ polymer_ligand_bonds,
31
+ ligand_ligand_bonds,
32
+ res_id,
33
+ chain_id,
34
+ )
35
+ # Connect the amino-acids, i.e. remove OXT, HXT and H2.
36
+ drop_atoms = atom_layout.get_link_drop_atoms(
37
+ res_name=res_name,
38
+ chain_type=chain_type,
39
+ is_start_terminus=False,
40
+ is_end_terminus=False,
41
+ bonded_atoms=bonded_atoms,
42
+ drop_ligand_leaving_atoms=True,
43
+ )
44
+ # Default mask where everything is false, which equates to being kept.
45
+ drop_atom_filter_atoms = struc.chain_id != struc.chain_id
46
+ for drop_atom in drop_atoms:
47
+ drop_atom_filter_atom = np.logical_and(
48
+ np.logical_and(
49
+ struc.atom_name == drop_atom,
50
+ struc.chain_id == chain_id,
51
+ ),
52
+ struc.res_id == res_id,
53
+ )
54
+ drop_atom_filter_atoms = np.logical_or(
55
+ drop_atom_filter_atoms, drop_atom_filter_atom
56
+ )
57
+ return drop_atom_filter_atoms
58
+
59
+
60
+ def clean_structure(
61
+ struc: structure.Structure,
62
+ ccd: chemical_components.Ccd,
63
+ *,
64
+ drop_missing_sequence: bool,
65
+ filter_clashes: bool,
66
+ drop_non_standard_atoms: bool,
67
+ filter_crystal_aids: bool,
68
+ filter_waters: bool,
69
+ filter_hydrogens: bool,
70
+ filter_leaving_atoms: bool,
71
+ only_glycan_ligands_for_leaving_atoms: bool,
72
+ covalent_bonds_only: bool,
73
+ remove_polymer_polymer_bonds: bool,
74
+ remove_bad_bonds: bool,
75
+ remove_nonsymmetric_bonds: bool,
76
+ ) -> tuple[structure.Structure, dict[str, Any]]:
77
+ """Cleans structure.
78
+
79
+ Args:
80
+ struc: Structure to clean.
81
+ ccd: The chemical components dictionary.
82
+ drop_missing_sequence: Whether to drop chains without specified sequences.
83
+ filter_clashes: Whether to drop clashing chains.
84
+ drop_non_standard_atoms: Whether to drop non CCD standard atoms.
85
+ filter_crystal_aids: Whether to drop ligands in the crystal aid set.
86
+ filter_waters: Whether to drop water chains.
87
+ filter_hydrogens: Whether to drop hyrdogen atoms.
88
+ filter_leaving_atoms: Whether to drop leaving atoms based on heuristics.
89
+ only_glycan_ligands_for_leaving_atoms: Whether to only include glycan
90
+ ligands when filtering leaving atoms.
91
+ covalent_bonds_only: Only include covalent bonds.
92
+ remove_polymer_polymer_bonds: Remove polymer-polymer bonds.
93
+ remove_bad_bonds: Whether to remove badly bonded ligands.
94
+ remove_nonsymmetric_bonds: Whether to remove nonsymmetric polymer-ligand
95
+ bonds from symmetric polymer chains.
96
+
97
+ Returns:
98
+ Tuple of structure and metadata dict. The metadata dict has
99
+ information about what was cleaned from the original.
100
+ """
101
+
102
+ metadata = {}
103
+ # Crop crystallization aids.
104
+ if (
105
+ filter_crystal_aids
106
+ and struc.structure_method in mmcif_names.CRYSTALLIZATION_METHODS
107
+ ):
108
+ struc = struc.filter_out(
109
+ res_name=chemical_component_sets.COMMON_CRYSTALLIZATION_AIDS
110
+ )
111
+
112
+ # Drop chains without specified sequences.
113
+ if drop_missing_sequence:
114
+ chains_with_unk_sequence = struc.find_chains_with_unknown_sequence()
115
+ num_with_unk_sequence = len(chains_with_unk_sequence)
116
+ if chains_with_unk_sequence:
117
+ struc = struc.filter_out(chain_id=chains_with_unk_sequence)
118
+ else:
119
+ num_with_unk_sequence = 0
120
+ metadata['num_with_unk_sequence'] = num_with_unk_sequence
121
+
122
+ # Remove intersecting chains.
123
+ if filter_clashes and struc.num_chains > 1:
124
+ clashing_chains = sterics.find_clashing_chains(struc)
125
+ if clashing_chains:
126
+ struc = struc.filter_out(chain_id=clashing_chains)
127
+ else:
128
+ clashing_chains = []
129
+ metadata['num_clashing_chains_removed'] = len(clashing_chains)
130
+ metadata['chains_removed'] = clashing_chains
131
+
132
+ # Drop non-standard atoms
133
+ if drop_non_standard_atoms:
134
+ struc = struc.drop_non_standard_atoms(
135
+ ccd=ccd, drop_unk=False, drop_non_ccd=False
136
+ )
137
+
138
+ # Sort chains in "reverse-spreadsheet" order.
139
+ struc = struc.with_sorted_chains
140
+
141
+ if filter_hydrogens:
142
+ struc = struc.without_hydrogen()
143
+
144
+ if filter_waters:
145
+ struc = struc.filter_out(chain_type=mmcif_names.WATER)
146
+
147
+ if filter_leaving_atoms:
148
+ drop_leaving_atoms_all = struc.chain_id != struc.chain_id
149
+ polymer_ligand_bonds = inter_chain_bonds.get_polymer_ligand_bonds(
150
+ struc,
151
+ only_glycan_ligands=only_glycan_ligands_for_leaving_atoms,
152
+ )
153
+ ligand_ligand_bonds = inter_chain_bonds.get_ligand_ligand_bonds(
154
+ struc,
155
+ only_glycan_ligands=only_glycan_ligands_for_leaving_atoms,
156
+ )
157
+ all_glycans = {
158
+ *chemical_component_sets.GLYCAN_OTHER_LIGANDS,
159
+ *chemical_component_sets.GLYCAN_LINKING_LIGANDS,
160
+ }
161
+ # If only glycan ligands and no O1 atoms, we can do parallel drop.
162
+ if (
163
+ only_glycan_ligands_for_leaving_atoms
164
+ and (not (ligand_ligand_bonds.atom_name == 'O1').any())
165
+ and (not (polymer_ligand_bonds.atom_name == 'O1').any())
166
+ ):
167
+ drop_leaving_atoms_all = np.logical_and(
168
+ np.isin(struc.atom_name, 'O1'),
169
+ np.isin(struc.res_name, list(all_glycans)),
170
+ )
171
+ else:
172
+ substruct = struc.group_by_residue
173
+ glycan_mask = np.isin(substruct.res_name, list(all_glycans))
174
+ substruct = substruct.filter(glycan_mask)
175
+ # We need to iterate over all glycan residues for this.
176
+ for res in substruct.iter_residues():
177
+ # Only need to do drop leaving atoms for glycans depending on bonds.
178
+ if (res_name := res['res_name']) in all_glycans:
179
+ drop_atom_filter = _get_leaving_atom_mask(
180
+ struc=struc,
181
+ polymer_ligand_bonds=polymer_ligand_bonds,
182
+ ligand_ligand_bonds=ligand_ligand_bonds,
183
+ chain_id=res['chain_id'],
184
+ chain_type=res['chain_type'],
185
+ res_id=res['res_id'],
186
+ res_name=res_name,
187
+ )
188
+ drop_leaving_atoms_all = np.logical_or(
189
+ drop_leaving_atoms_all, drop_atom_filter
190
+ )
191
+
192
+ num_atoms_before = struc.num_atoms
193
+ struc = struc.filter_out(drop_leaving_atoms_all)
194
+ num_atoms_after = struc.num_atoms
195
+
196
+ if num_atoms_before > num_atoms_after:
197
+ logging.error(
198
+ 'Dropped %s atoms from GT struc: chain_id %s res_id %s res_name %s',
199
+ num_atoms_before - num_atoms_after,
200
+ struc.chain_id,
201
+ struc.res_id,
202
+ struc.res_name,
203
+ )
204
+
205
+ # Can filter by bond type without having to iterate over bonds.
206
+ if struc.bonds and covalent_bonds_only:
207
+ is_covalent = np.isin(struc.bonds.type, ['covale'])
208
+ if sum(is_covalent) > 0:
209
+ new_bonds = struc.bonds[is_covalent]
210
+ else:
211
+ new_bonds = structure.Bonds.make_empty()
212
+ struc = struc.copy_and_update(bonds=new_bonds)
213
+
214
+ # Other bond filters require iterating over individual bonds.
215
+ if struc.bonds and (remove_bad_bonds or remove_polymer_polymer_bonds):
216
+ include_bond = []
217
+ num_pp_bonds = 0
218
+ num_bad_bonds = 0
219
+ for bond in struc.iter_bonds():
220
+ dest_atom = bond.dest_atom
221
+ from_atom = bond.from_atom
222
+ if remove_polymer_polymer_bonds:
223
+ if (
224
+ from_atom['chain_type'] in mmcif_names.POLYMER_CHAIN_TYPES
225
+ and dest_atom['chain_type'] in mmcif_names.POLYMER_CHAIN_TYPES
226
+ ):
227
+ num_pp_bonds += 1
228
+ include_bond.append(False)
229
+ continue
230
+ if remove_bad_bonds:
231
+ dest_coords = np.array(
232
+ [dest_atom['atom_x'], dest_atom['atom_y'], dest_atom['atom_z']]
233
+ )
234
+ from_coords = np.array(
235
+ [from_atom['atom_x'], from_atom['atom_y'], from_atom['atom_z']]
236
+ )
237
+ squared_dist = np.sum(np.square(dest_coords - from_coords))
238
+ squared_threshold = 2.4 * 2.4
239
+ if squared_dist > squared_threshold:
240
+ num_bad_bonds += 1
241
+ include_bond.append(False)
242
+ continue
243
+ include_bond.append(True)
244
+ if sum(include_bond) < len(struc.bonds):
245
+ logging.info(
246
+ 'Reducing number of bonds for %s from %s to %s, of which %s are'
247
+ ' polymer-polymer bonds and %s are bad bonds.',
248
+ struc.name,
249
+ len(struc.bonds),
250
+ sum(include_bond),
251
+ num_pp_bonds,
252
+ num_bad_bonds,
253
+ )
254
+ if sum(include_bond) > 0:
255
+ # Need to index bonds with bond keys or arrays of bools with same length
256
+ # as num bonds. In this case, we use array of bools (as elsewhere in the
257
+ # cleaning code).
258
+ new_bonds = struc.bonds[np.array(include_bond, dtype=bool)]
259
+ else:
260
+ new_bonds = structure.Bonds.make_empty()
261
+ struc = struc.copy_and_update(bonds=new_bonds)
262
+
263
+ if struc.bonds and remove_nonsymmetric_bonds:
264
+ # Check for asymmetric polymer-ligand bonds and remove if these exist.
265
+ polymer_ligand_bonds = inter_chain_bonds.get_polymer_ligand_bonds(
266
+ struc,
267
+ only_glycan_ligands=False,
268
+ )
269
+ if polymer_ligand_bonds:
270
+ if covalent_bond_cleaning.has_nonsymmetric_bonds_on_symmetric_polymer_chains(
271
+ struc, polymer_ligand_bonds
272
+ ):
273
+ from_atom_idxs, dest_atom_idxs = struc.bonds.get_atom_indices(
274
+ struc.atom_key
275
+ )
276
+ poly_chain_types = list(mmcif_names.POLYMER_CHAIN_TYPES)
277
+ is_polymer_bond = np.logical_or(
278
+ np.isin(struc.chain_type[from_atom_idxs], poly_chain_types),
279
+ np.isin(struc.chain_type[dest_atom_idxs], poly_chain_types),
280
+ )
281
+ struc = struc.copy_and_update(bonds=struc.bonds[~is_polymer_bond])
282
+
283
+ return struc, metadata
284
+
285
+
286
+ def create_empty_output_struc_and_layout(
287
+ struc: structure.Structure,
288
+ ccd: chemical_components.Ccd,
289
+ *,
290
+ with_hydrogens: bool = False,
291
+ skip_unk: bool = False,
292
+ polymer_ligand_bonds: atom_layout.AtomLayout | None = None,
293
+ ligand_ligand_bonds: atom_layout.AtomLayout | None = None,
294
+ drop_ligand_leaving_atoms: bool = False,
295
+ ) -> tuple[structure.Structure, atom_layout.AtomLayout]:
296
+ """Make zero-coordinate structure from all physical residues.
297
+
298
+ Args:
299
+ struc: Structure object.
300
+ ccd: The chemical components dictionary.
301
+ with_hydrogens: Whether to keep hydrogen atoms in structure.
302
+ skip_unk: Whether to remove unknown residues from structure.
303
+ polymer_ligand_bonds: Bond information for polymer-ligand pairs.
304
+ ligand_ligand_bonds: Bond information for ligand-ligand pairs.
305
+ drop_ligand_leaving_atoms: Flag for handling leaving atoms for ligands.
306
+
307
+ Returns:
308
+ Tuple of structure with all bonds, physical residues and coordinates set to
309
+ 0 and a flat atom layout of empty structure.
310
+ """
311
+ bonded_atom_pairs = []
312
+ if polymer_ligand_bonds:
313
+ for chain_ids, res_ids, atom_names in zip(
314
+ polymer_ligand_bonds.chain_id,
315
+ polymer_ligand_bonds.res_id,
316
+ polymer_ligand_bonds.atom_name,
317
+ strict=True,
318
+ ):
319
+ bonded_atom_pairs.append((
320
+ (chain_ids[0], res_ids[0], atom_names[0]),
321
+ (chain_ids[1], res_ids[1], atom_names[1]),
322
+ ))
323
+ if ligand_ligand_bonds:
324
+ for chain_ids, res_ids, atom_names in zip(
325
+ ligand_ligand_bonds.chain_id,
326
+ ligand_ligand_bonds.res_id,
327
+ ligand_ligand_bonds.atom_name,
328
+ strict=True,
329
+ ):
330
+ bonded_atom_pairs.append((
331
+ (chain_ids[0], res_ids[0], atom_names[0]),
332
+ (chain_ids[1], res_ids[1], atom_names[1]),
333
+ ))
334
+ residues = atom_layout.residues_from_structure(
335
+ struc, include_missing_residues=True
336
+ )
337
+
338
+ flat_output_layout = atom_layout.make_flat_atom_layout(
339
+ residues,
340
+ ccd=ccd,
341
+ with_hydrogens=with_hydrogens,
342
+ skip_unk_residues=skip_unk,
343
+ polymer_ligand_bonds=polymer_ligand_bonds,
344
+ ligand_ligand_bonds=ligand_ligand_bonds,
345
+ drop_ligand_leaving_atoms=drop_ligand_leaving_atoms,
346
+ )
347
+
348
+ empty_output_struc = atom_layout.make_structure(
349
+ flat_layout=flat_output_layout,
350
+ atom_coords=np.zeros((flat_output_layout.shape[0], 3)),
351
+ name=struc.name,
352
+ atom_b_factors=None,
353
+ all_physical_residues=residues,
354
+ )
355
+ if bonded_atom_pairs:
356
+ empty_output_struc = empty_output_struc.add_bonds(
357
+ bonded_atom_pairs, bond_type=mmcif_names.COVALENT_BOND
358
+ )
359
+
360
+ return empty_output_struc, flat_output_layout
flax_model/alphafold3/model/protein_data_processing.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """Process Structure Data."""
4
+
5
+ from flax_model.alphafold3.constants import atom_types
6
+ from flax_model.alphafold3.constants import residue_names
7
+ from flax_model.alphafold3.constants import side_chains
8
+ import numpy as np
9
+
10
+
11
+ NUM_DENSE = atom_types.DENSE_ATOM_NUM
12
+ NUM_AA = len(residue_names.PROTEIN_TYPES)
13
+ NUM_AA_WITH_UNK_AND_GAP = len(
14
+ residue_names.PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP
15
+ )
16
+ NUM_RESTYPES_WITH_UNK_AND_GAP = (
17
+ residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP
18
+ )
19
+
20
+
21
+ def _make_restype_rigidgroup_dense_atom_idx():
22
+ """Create Mapping from rigid_groups to dense_atom indices."""
23
+ # Create an array with the atom names.
24
+ # shape (num_restypes, num_rigidgroups, 3_atoms):
25
+ # (31, 8, 3)
26
+ base_atom_indices = np.zeros(
27
+ (NUM_RESTYPES_WITH_UNK_AND_GAP, 8, 3), dtype=np.int32
28
+ )
29
+
30
+ # 4,5,6,7: 'chi1,2,3,4-group'
31
+ for restype, restype_letter in enumerate(
32
+ residue_names.PROTEIN_TYPES_ONE_LETTER
33
+ ):
34
+ resname = residue_names.PROTEIN_COMMON_ONE_TO_THREE[restype_letter]
35
+
36
+ dense_atom_names = atom_types.ATOM14[resname]
37
+ # 0: backbone frame
38
+ base_atom_indices[restype, 0, :] = [
39
+ dense_atom_names.index(atom) for atom in ['C', 'CA', 'N']
40
+ ]
41
+
42
+ # 3: 'psi-group'
43
+ base_atom_indices[restype, 3, :] = [
44
+ dense_atom_names.index(atom) for atom in ['CA', 'C', 'O']
45
+ ]
46
+ for chi_idx in range(4):
47
+ if side_chains.CHI_ANGLES_MASK[restype][chi_idx]:
48
+ atom_names = side_chains.CHI_ANGLES_ATOMS[resname][chi_idx]
49
+ base_atom_indices[restype, chi_idx + 4, :] = [
50
+ dense_atom_names.index(atom) for atom in atom_names[1:]
51
+ ]
52
+ dense_atom_names = atom_types.DENSE_ATOM['A']
53
+ nucleic_rigid_atoms = [
54
+ dense_atom_names.index(atom) for atom in ["C1'", "C3'", "C4'"]
55
+ ]
56
+ for nanum, _ in enumerate(residue_names.NUCLEIC_TYPES):
57
+ # 0: backbone frame only.
58
+ # we have aa + unk + gap, so we want to start after those
59
+ resnum = nanum + NUM_AA_WITH_UNK_AND_GAP
60
+ base_atom_indices[resnum, 0, :] = nucleic_rigid_atoms
61
+
62
+ return base_atom_indices
63
+
64
+
65
+ RESTYPE_RIGIDGROUP_DENSE_ATOM_IDX = _make_restype_rigidgroup_dense_atom_idx()
66
+
67
+
68
+ def _make_restype_pseudobeta_idx():
69
+ """Returns indices of residue's pseudo-beta."""
70
+ restype_pseudobeta_index = np.zeros(
71
+ (NUM_RESTYPES_WITH_UNK_AND_GAP,), dtype=np.int32
72
+ )
73
+ for restype, restype_letter in enumerate(
74
+ residue_names.PROTEIN_TYPES_ONE_LETTER
75
+ ):
76
+ restype_name = residue_names.PROTEIN_COMMON_ONE_TO_THREE[restype_letter]
77
+ atom_names = list(atom_types.ATOM14[restype_name])
78
+ if restype_name in {'GLY'}:
79
+ restype_pseudobeta_index[restype] = atom_names.index('CA')
80
+ else:
81
+ restype_pseudobeta_index[restype] = atom_names.index('CB')
82
+ for nanum, resname in enumerate(residue_names.NUCLEIC_TYPES):
83
+ atom_names = list(atom_types.DENSE_ATOM[resname])
84
+ # 0: backbone frame only.
85
+ # we have aa + unk , so we want to start after those
86
+ restype = nanum + NUM_AA_WITH_UNK_AND_GAP
87
+ if resname in {'A', 'G', 'DA', 'DG'}:
88
+ restype_pseudobeta_index[restype] = atom_names.index('C4')
89
+ else:
90
+ restype_pseudobeta_index[restype] = atom_names.index('C2')
91
+ return restype_pseudobeta_index
92
+
93
+
94
+ RESTYPE_PSEUDOBETA_INDEX = _make_restype_pseudobeta_idx()
95
+
96
+
97
+ def _make_aatype_dense_atom_to_atom37():
98
+ """Map from dense_atom to atom37 per residue type."""
99
+ restype_dense_atom_to_atom37 = [] # mapping (restype, dense_atom) --> atom37
100
+ for rt in residue_names.PROTEIN_TYPES_ONE_LETTER:
101
+ atom_names = list(
102
+ atom_types.ATOM14_PADDED[residue_names.PROTEIN_COMMON_ONE_TO_THREE[rt]]
103
+ )
104
+ atom_names.extend([''] * (NUM_DENSE - len(atom_names)))
105
+ restype_dense_atom_to_atom37.append(
106
+ [(atom_types.ATOM37_ORDER[name] if name else 0) for name in atom_names]
107
+ )
108
+ # Add dummy mapping for restype 'UNK', '-' (gap), and nucleics [but not DN].
109
+ for _ in range(2 + len(residue_names.NUCLEIC_TYPES_WITH_UNKNOWN)):
110
+ restype_dense_atom_to_atom37.append([0] * NUM_DENSE)
111
+
112
+ restype_dense_atom_to_atom37 = np.array(
113
+ restype_dense_atom_to_atom37, dtype=np.int32
114
+ )
115
+ return restype_dense_atom_to_atom37
116
+
117
+
118
+ PROTEIN_AATYPE_DENSE_ATOM_TO_ATOM37 = _make_aatype_dense_atom_to_atom37()
flax_model/alphafold3/model/scoring/__init__.py ADDED
File without changes