Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- flax_model/alphafold3/_data/libcifpp/mmcif_ddl.dic +0 -0
- flax_model/alphafold3/_data/libcifpp/mmcif_ma.dic +0 -0
- flax_model/alphafold3/_data/libcifpp/mmcif_pdbx.dic +0 -0
- flax_model/alphafold3/_tools/hmmer/.built +1 -0
- flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-compstruct.1 +134 -0
- flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-construct.1 +196 -0
- flax_model/alphafold3/_tools/hmmer/share/man/man1/esl-seqrange.1 +99 -0
- flax_model/alphafold3/data/tools/hmmalign.py +134 -0
- flax_model/alphafold3/jax/attention/attention.py +150 -0
- flax_model/alphafold3/jax/common/array_view.py +397 -0
- flax_model/alphafold3/jax/common/precision.py +84 -0
- flax_model/alphafold3/jax/gated_linear_unit/__init__.py +0 -0
- flax_model/alphafold3/jax/gated_linear_unit/gated_linear_unit_base.py +122 -0
- flax_model/alphafold3/jax/gated_linear_unit/matmul_ext.py +265 -0
- flax_model/alphafold3/jax/geometry/rigid_matrix_vector.py +216 -0
- flax_model/alphafold3/jax/geometry/rotation_matrix.py +283 -0
- flax_model/alphafold3/jax/geometry/struct_of_array.py +223 -0
- flax_model/alphafold3/jax/geometry/utils.py +135 -0
- flax_model/alphafold3/jax/geometry/vector.py +214 -0
- flax_model/alphafold3/model/__init__.py +0 -0
- flax_model/alphafold3/model/atom_layout/__init__.py +0 -0
- flax_model/alphafold3/model/atom_layout/atom_layout.py +1087 -0
- flax_model/alphafold3/model/components/haiku_modules.py +328 -0
- flax_model/alphafold3/model/components/mapping.py +240 -0
- flax_model/alphafold3/model/components/utils.py +75 -0
- flax_model/alphafold3/model/confidence_types.py +293 -0
- flax_model/alphafold3/model/confidences.py +655 -0
- flax_model/alphafold3/model/data3.py +117 -0
- flax_model/alphafold3/model/data_constants.py +19 -0
- flax_model/alphafold3/model/feat_batch.py +77 -0
- flax_model/alphafold3/model/features.py +2159 -0
- flax_model/alphafold3/model/mkdssp_pybind.cc +63 -0
- flax_model/alphafold3/model/mkdssp_pybind.h +26 -0
- flax_model/alphafold3/model/mmcif_metadata.py +222 -0
- flax_model/alphafold3/model/model.py +533 -0
- flax_model/alphafold3/model/model_config.py +24 -0
- flax_model/alphafold3/model/msa_pairing.py +304 -0
- flax_model/alphafold3/model/network/atom_cross_attention.py +413 -0
- flax_model/alphafold3/model/network/confidence_head.py +312 -0
- flax_model/alphafold3/model/network/diffusion_head.py +363 -0
- flax_model/alphafold3/model/network/diffusion_transformer.py +396 -0
- flax_model/alphafold3/model/network/distogram_head.py +75 -0
- flax_model/alphafold3/model/network/featurization.py +261 -0
- flax_model/alphafold3/model/network/noise_level_embeddings.py +133 -0
- flax_model/alphafold3/model/network/template_modules.py +343 -0
- flax_model/alphafold3/model/params.py +202 -0
- flax_model/alphafold3/model/pipeline/__init__.py +0 -0
- flax_model/alphafold3/model/pipeline/structure_cleaning.py +360 -0
- flax_model/alphafold3/model/protein_data_processing.py +118 -0
- 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
|