Upload folder using huggingface_hub (part 2)
Browse files- model/boltzgen/resources/config/train/boltzgen.no_distillation.yaml +484 -0
- model/boltzgen/resources/config/train/boltzgen.yaml +578 -0
- model/boltzgen/resources/config/train/boltzgen_small.yaml +346 -0
- model/boltzgen/resources/config/train/inverse_folding.yaml +376 -0
- model/boltzgen/resources/main.py +53 -0
- model/boltzgen/resources/metrics_normalization.json +38 -0
- model/boltzgen/resources/splits/val_ccd_pdb_pairs_boltzgen.txt +101 -0
- model/boltzgen/resources/splits/val_monomers_boltzgen_min50_max220.txt +110 -0
- model/boltzgen/resources/splits/validation_ids_boltz2_all.txt +398 -0
- model/boltzgen/task/__init__.py +0 -0
- model/boltzgen/task/analyze/analyze.py +1483 -0
- model/boltzgen/task/analyze/analyze_utils.py +1260 -0
- model/boltzgen/task/filter/__init__.py +0 -0
- model/boltzgen/task/filter/filter.py +1413 -0
- model/boltzgen/task/filter/seqplot_utils.py +505 -0
- model/boltzgen/task/predict/__init__.py +0 -0
- model/boltzgen/task/predict/data_from_generated.py +882 -0
- model/boltzgen/task/predict/data_from_yaml.py +449 -0
- model/boltzgen/task/predict/data_ligands.py +410 -0
- model/boltzgen/task/predict/data_protein_binder.py +608 -0
- model/boltzgen/task/predict/loading_utils.py +45 -0
- model/boltzgen/task/predict/predict.py +194 -0
- model/boltzgen/task/predict/writer.py +534 -0
- model/boltzgen/task/task.py +19 -0
- model/boltzgen/task/train/__init__.py +0 -0
- model/boltzgen/task/train/data.py +1259 -0
- model/boltzgen/task/train/train.py +250 -0
- model/boltzgen/utils/__init__.py +0 -0
- model/boltzgen/utils/pipeline_progress_bar.py +171 -0
- model/boltzgen/utils/quiet.py +16 -0
- requirements.txt +53 -0
- scripts/boltzgen.py +22 -0
- scripts/check_environment.py +55 -0
- scripts/filter.ipynb +0 -0
- scripts/slurm-example/README.md +28 -0
- scripts/slurm-example/run.sh +42 -0
- scripts/slurm-example/run_job_array.slurm +53 -0
- scripts/train.py +28 -0
model/boltzgen/resources/config/train/boltzgen.no_distillation.yaml
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: boltzgen.task.train.train.Training
|
| 2 |
+
|
| 3 |
+
trainer:
|
| 4 |
+
accelerator: gpu
|
| 5 |
+
devices: 8
|
| 6 |
+
precision: bf16-mixed
|
| 7 |
+
gradient_clip_val: 10.0
|
| 8 |
+
accumulate_grad_batches: 1
|
| 9 |
+
max_epochs: -1
|
| 10 |
+
num_sanity_val_steps: 3
|
| 11 |
+
log_every_n_steps: 1
|
| 12 |
+
|
| 13 |
+
wandb:
|
| 14 |
+
group: boltzgen
|
| 15 |
+
project: boltzgen
|
| 16 |
+
entity: yourwandb
|
| 17 |
+
|
| 18 |
+
name: a_big_run_resume3
|
| 19 |
+
slurm: true
|
| 20 |
+
output: workdir
|
| 21 |
+
strict_loading: false
|
| 22 |
+
resume: null
|
| 23 |
+
pretrained: null
|
| 24 |
+
debug: false
|
| 25 |
+
save_every_n_train_steps: 2500
|
| 26 |
+
disable_checkpoint: false
|
| 27 |
+
matmul_precision: null
|
| 28 |
+
save_top_k: -1
|
| 29 |
+
|
| 30 |
+
data:
|
| 31 |
+
datasets:
|
| 32 |
+
- _target_: boltzgen.task.train.data.DatasetConfig
|
| 33 |
+
target_dir: ./training_data/targets
|
| 34 |
+
msa_dir: ./training_data/msa
|
| 35 |
+
prob: 1.0
|
| 36 |
+
filters:
|
| 37 |
+
- _target_: boltzgen.data.filter.dynamic.size.SizeFilter
|
| 38 |
+
min_chains: 1
|
| 39 |
+
max_chains: 300
|
| 40 |
+
- _target_: boltzgen.data.filter.dynamic.date.DateFilter
|
| 41 |
+
date: "2023-06-01"
|
| 42 |
+
ref: released
|
| 43 |
+
- _target_: boltzgen.data.filter.dynamic.resolution.ResolutionFilter
|
| 44 |
+
resolution: 9.0
|
| 45 |
+
sampler:
|
| 46 |
+
_target_: boltzgen.data.sample.cluster.ClusterSampler
|
| 47 |
+
cropper:
|
| 48 |
+
_target_: boltzgen.data.crop.multimer.MultimerCropper
|
| 49 |
+
neighborhood_sizes: [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 ]
|
| 50 |
+
split: ./model/boltzgen/resources/splits/validation_ids_boltz2_all.txt
|
| 51 |
+
symmetry_correction: false
|
| 52 |
+
val_group: "RCSB"
|
| 53 |
+
|
| 54 |
+
tokenizer:
|
| 55 |
+
_target_: boltzgen.data.tokenize.tokenizer.Tokenizer
|
| 56 |
+
atomize_modified_residues: false
|
| 57 |
+
featurizer:
|
| 58 |
+
_target_: boltzgen.data.feature.featurizer.Featurizer
|
| 59 |
+
moldir: ./training_data/mols
|
| 60 |
+
max_tokens: 512
|
| 61 |
+
max_atoms: 5120
|
| 62 |
+
max_seqs: 4096
|
| 63 |
+
pad_to_max_tokens: true
|
| 64 |
+
pad_to_max_atoms: true
|
| 65 |
+
pad_to_max_seqs: true
|
| 66 |
+
samples_per_epoch: 100000
|
| 67 |
+
batch_size: 1
|
| 68 |
+
num_workers: 2
|
| 69 |
+
random_seed: 42
|
| 70 |
+
pin_memory: false
|
| 71 |
+
overfit: null
|
| 72 |
+
return_train_symmetries: false
|
| 73 |
+
return_val_symmetries: false
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
atoms_per_window_queries: 32
|
| 77 |
+
min_dist: 2.0
|
| 78 |
+
max_dist: 22.0
|
| 79 |
+
num_bins: 64
|
| 80 |
+
single_sequence_prop_training: 0.1
|
| 81 |
+
msa_sampling_training: true
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# Design
|
| 85 |
+
design: true
|
| 86 |
+
backbone_only: false
|
| 87 |
+
atom14: true
|
| 88 |
+
atom37: false
|
| 89 |
+
selector:
|
| 90 |
+
_target_: boltzgen.data.select.protein.ProteinSelector
|
| 91 |
+
design_neighborhood_sizes: [2, 4, 6,8,10,12,14,16,18]
|
| 92 |
+
substructure_neighborhood_sizes: [2,4,6,8,10,12,24]
|
| 93 |
+
structure_condition_prob: 0.4
|
| 94 |
+
distance_noise_std: 1
|
| 95 |
+
run_selection: true
|
| 96 |
+
specify_binding_sites: true
|
| 97 |
+
ss_condition_prob: 0.1
|
| 98 |
+
select_all: false
|
| 99 |
+
|
| 100 |
+
# Design datasets
|
| 101 |
+
monomer_split: ./model/boltzgen/resources/splits/val_monomers_boltzgen_min50_max220.txt
|
| 102 |
+
monomer_target_dir: ./training_data/targets
|
| 103 |
+
monomer_target_structure_condition: true
|
| 104 |
+
monomer_seq_len: 100
|
| 105 |
+
|
| 106 |
+
ligand_split: ./model/boltzgen/resources/splits/val_ccd_pdb_pairs_boltzgen.txt
|
| 107 |
+
ligand_target_dir: ./training_data/targets
|
| 108 |
+
ligand_seq_len: 100
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
model:
|
| 112 |
+
_target_: boltzgen.model.models.boltz.Boltz
|
| 113 |
+
atom_s: 128
|
| 114 |
+
atom_z: 16
|
| 115 |
+
token_s: 384
|
| 116 |
+
token_z: 128
|
| 117 |
+
num_bins: 64
|
| 118 |
+
atom_feature_dim: 388
|
| 119 |
+
atoms_per_window_queries: 32
|
| 120 |
+
atoms_per_window_keys: 128
|
| 121 |
+
use_miniformer: false
|
| 122 |
+
ema: true
|
| 123 |
+
ema_decay: 0.999
|
| 124 |
+
exclude_ions_from_lddt: true
|
| 125 |
+
num_val_datasets: 1 # New
|
| 126 |
+
ignore_ckpt_shape_mismatch: false # New
|
| 127 |
+
aggregate_distogram: true # New
|
| 128 |
+
bond_type_feature: true
|
| 129 |
+
predict_bfactor: true
|
| 130 |
+
checkpoint_diffusion_conditioning: true
|
| 131 |
+
use_kernels: true
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
validators:
|
| 135 |
+
- _target_: boltzgen.model.validation.design.DesignValidator
|
| 136 |
+
val_names: ["RCSB"]
|
| 137 |
+
confidence_prediction: ${model.confidence_prediction}
|
| 138 |
+
atom14: ${data.atom14}
|
| 139 |
+
atom37: ${data.atom37}
|
| 140 |
+
|
| 141 |
+
masker_args:
|
| 142 |
+
mask: true
|
| 143 |
+
mask_backbone: false
|
| 144 |
+
mask_disto: true
|
| 145 |
+
|
| 146 |
+
embedder_args:
|
| 147 |
+
atom_encoder_depth: 3
|
| 148 |
+
atom_encoder_heads: 4
|
| 149 |
+
add_mol_type_feat: true
|
| 150 |
+
add_method_conditioning: true
|
| 151 |
+
add_modified_flag: true
|
| 152 |
+
add_cyclic_flag: true
|
| 153 |
+
add_design_mask_flag: true
|
| 154 |
+
add_binding_specification: true
|
| 155 |
+
add_ss_specification: true
|
| 156 |
+
|
| 157 |
+
freeze_template_weights: true
|
| 158 |
+
use_templates: true
|
| 159 |
+
template_args:
|
| 160 |
+
template_dim: 64
|
| 161 |
+
template_blocks: 2
|
| 162 |
+
activation_checkpointing: false
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
use_token_distances: true
|
| 166 |
+
token_distance_args:
|
| 167 |
+
token_distance_dim: 64
|
| 168 |
+
token_distance_blocks: 2
|
| 169 |
+
use_token_distance_feats: true
|
| 170 |
+
distance_gaussian_dim: 32
|
| 171 |
+
activation_checkpointing: true
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
msa_args:
|
| 175 |
+
msa_s: 64
|
| 176 |
+
msa_blocks: 4
|
| 177 |
+
msa_dropout: 0.15
|
| 178 |
+
z_dropout: 0.25
|
| 179 |
+
miniformer_blocks: false
|
| 180 |
+
pairwise_head_width: 32
|
| 181 |
+
pairwise_num_heads: 4
|
| 182 |
+
use_paired_feature: true
|
| 183 |
+
activation_checkpointing: true
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
pairformer_args:
|
| 187 |
+
num_blocks: 64
|
| 188 |
+
num_heads: 16
|
| 189 |
+
dropout: 0.25
|
| 190 |
+
post_layer_norm: false
|
| 191 |
+
activation_checkpointing: true
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
score_model_args:
|
| 195 |
+
sigma_data: 16
|
| 196 |
+
dim_fourier: 256
|
| 197 |
+
atom_encoder_depth: 3
|
| 198 |
+
atom_encoder_heads: 4
|
| 199 |
+
|
| 200 |
+
# token level args
|
| 201 |
+
token_layers: 1
|
| 202 |
+
token_transformer_depth: 24
|
| 203 |
+
token_transformer_heads: 16
|
| 204 |
+
diffusion_pairformer_args:
|
| 205 |
+
num_blocks: 0
|
| 206 |
+
num_heads: 2
|
| 207 |
+
dropout: 0
|
| 208 |
+
use_s_to_z: false
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
atom_decoder_depth: 3
|
| 213 |
+
atom_decoder_heads: 4
|
| 214 |
+
conditioning_transition_layers: 2
|
| 215 |
+
transformer_post_ln: false
|
| 216 |
+
activation_checkpointing: true
|
| 217 |
+
|
| 218 |
+
confidence_prediction: false
|
| 219 |
+
structure_prediction_training: true
|
| 220 |
+
|
| 221 |
+
training_args:
|
| 222 |
+
recycling_steps: 3
|
| 223 |
+
sampling_steps: 20
|
| 224 |
+
diffusion_multiplicity: 32
|
| 225 |
+
diffusion_samples: 1
|
| 226 |
+
confidence_loss_weight: 1e-4
|
| 227 |
+
diffusion_loss_weight: 4.0
|
| 228 |
+
distogram_loss_weight: 3e-2
|
| 229 |
+
bfactor_loss_weight: 1e-3
|
| 230 |
+
adam_beta_1: 0.9
|
| 231 |
+
adam_beta_2: 0.95
|
| 232 |
+
adam_eps: 0.00000001
|
| 233 |
+
lr_scheduler: af3
|
| 234 |
+
base_lr: 0.0
|
| 235 |
+
max_lr: 0.0005
|
| 236 |
+
lr_warmup_no_steps: 1000
|
| 237 |
+
lr_start_decay_after_n_steps: 50000
|
| 238 |
+
lr_decay_every_n_steps: 50000
|
| 239 |
+
lr_decay_factor: 0.95
|
| 240 |
+
weight_decay: 0.003
|
| 241 |
+
weight_decay_exclude: true
|
| 242 |
+
|
| 243 |
+
validation_args:
|
| 244 |
+
recycling_steps: 3
|
| 245 |
+
sampling_steps: 200
|
| 246 |
+
diffusion_samples: 1
|
| 247 |
+
symmetry_correction: false
|
| 248 |
+
|
| 249 |
+
diffusion_process_args:
|
| 250 |
+
sigma_min: 0.0004 # min noise level
|
| 251 |
+
sigma_max: 160.0 # max noise level
|
| 252 |
+
sigma_data: 16.0 # standard deviation of data distribution
|
| 253 |
+
rho: 7 # controls the sampling schedule
|
| 254 |
+
P_mean: -1.2 # mean of log-normal distribution from which noise is drawn for training
|
| 255 |
+
P_std: 1.5 # standard deviation of log-normal distribution from which noise is drawn for training
|
| 256 |
+
gamma_0: 0.8
|
| 257 |
+
gamma_min: 1.0
|
| 258 |
+
noise_scale: 1.0
|
| 259 |
+
step_scale: 1.0
|
| 260 |
+
mse_rotational_alignment: true
|
| 261 |
+
coordinate_augmentation: true
|
| 262 |
+
alignment_reverse_diff: true
|
| 263 |
+
synchronize_sigmas: false
|
| 264 |
+
|
| 265 |
+
diffusion_loss_args:
|
| 266 |
+
add_smooth_lddt_loss: true
|
| 267 |
+
add_bond_loss: false
|
| 268 |
+
nucleotide_loss_weight: 5.0
|
| 269 |
+
ligand_loss_weight: 10.0
|
| 270 |
+
|
| 271 |
+
refolding_validator:
|
| 272 |
+
_target_: boltzgen.model.validation.refolding.RefoldingValidator
|
| 273 |
+
val_names: ["RCSB"]
|
| 274 |
+
step_scale: 1.5
|
| 275 |
+
noise_scale: 0.75
|
| 276 |
+
atom14: ${data.atom14}
|
| 277 |
+
atom37: ${data.atom37}
|
| 278 |
+
val_monomer: ${data.monomer_split}
|
| 279 |
+
val_ligand: ${data.ligand_split}
|
| 280 |
+
analyze_task:
|
| 281 |
+
_target_: boltzgen.task.analyze.analyze.Analyze
|
| 282 |
+
name: ${name}
|
| 283 |
+
debug: ${debug}
|
| 284 |
+
design_dir: null
|
| 285 |
+
num_processes: 1
|
| 286 |
+
|
| 287 |
+
# Common metrics to compute
|
| 288 |
+
affinity_metrics: false
|
| 289 |
+
allatom_fold_metrics: true
|
| 290 |
+
backbone_fold_metrics: true
|
| 291 |
+
noncovalents_original: false
|
| 292 |
+
noncovalents_refolded: false
|
| 293 |
+
delta_sasa_original: false
|
| 294 |
+
delta_sasa_refolded: false
|
| 295 |
+
largest_hydrophobic: false
|
| 296 |
+
largest_hydrophobic_refolded: false
|
| 297 |
+
run_clustering: false
|
| 298 |
+
|
| 299 |
+
# Liability analysis
|
| 300 |
+
liability_analysis: false
|
| 301 |
+
liability_modality: peptide
|
| 302 |
+
liability_peptide_type: linear
|
| 303 |
+
|
| 304 |
+
# Uncommon metrics
|
| 305 |
+
diversity_original: true
|
| 306 |
+
diversity_refolded: true
|
| 307 |
+
diversity_per_target_original: false
|
| 308 |
+
diversity_per_target_refolded: false
|
| 309 |
+
novelty_original: false
|
| 310 |
+
novelty_refolded: false
|
| 311 |
+
novelty_per_target_original: false
|
| 312 |
+
novelty_per_target_refolded: false
|
| 313 |
+
|
| 314 |
+
wandb: null
|
| 315 |
+
|
| 316 |
+
data:
|
| 317 |
+
_target_: boltzgen.task.predict.data_from_generated.FromGeneratedDataModule
|
| 318 |
+
cfg:
|
| 319 |
+
_target_: boltzgen.task.predict.data_from_generated.DataConfig
|
| 320 |
+
tokenizer:
|
| 321 |
+
_target_: boltzgen.data.tokenize.tokenizer.Tokenizer
|
| 322 |
+
atomize_modified_residues: false
|
| 323 |
+
featurizer:
|
| 324 |
+
_target_: boltzgen.data.feature.featurizer.Featurizer
|
| 325 |
+
|
| 326 |
+
suffix: .cif
|
| 327 |
+
suffix_metadata: .npz
|
| 328 |
+
suffix_native: _native.cif
|
| 329 |
+
samples_per_target: 1
|
| 330 |
+
num_targets: 100000000
|
| 331 |
+
moldir: ./training_data/mols
|
| 332 |
+
|
| 333 |
+
batch_size: 1
|
| 334 |
+
num_workers: 4
|
| 335 |
+
pin_memory: true
|
| 336 |
+
return_native: true
|
| 337 |
+
|
| 338 |
+
folding_checkpoint: ./training_data/boltz2_fold.ckpt
|
| 339 |
+
|
| 340 |
+
folding_args:
|
| 341 |
+
recycling_steps: 3
|
| 342 |
+
sampling_steps: 200
|
| 343 |
+
diffusion_samples: 1
|
| 344 |
+
|
| 345 |
+
folding_model_args:
|
| 346 |
+
atom_s: 128
|
| 347 |
+
atom_z: 16
|
| 348 |
+
token_s: 384
|
| 349 |
+
token_z: 128
|
| 350 |
+
num_bins: 64
|
| 351 |
+
atom_feature_dim: 388
|
| 352 |
+
atoms_per_window_queries: 32
|
| 353 |
+
atoms_per_window_keys: 128
|
| 354 |
+
compile_pairformer: false
|
| 355 |
+
compile_templates: false
|
| 356 |
+
compile_msa: false
|
| 357 |
+
use_miniformer: false
|
| 358 |
+
ema: true
|
| 359 |
+
ema_decay: 0.999
|
| 360 |
+
exclude_ions_from_lddt: true
|
| 361 |
+
num_val_datasets: 4
|
| 362 |
+
ignore_ckpt_shape_mismatch: false
|
| 363 |
+
aggregate_distogram: true
|
| 364 |
+
bond_type_feature: true
|
| 365 |
+
conditioning_cutoff_min: 4.0
|
| 366 |
+
conditioning_cutoff_max: 20.0
|
| 367 |
+
use_templates: true
|
| 368 |
+
predict_bfactor: true
|
| 369 |
+
checkpoint_diffusion_conditioning: false
|
| 370 |
+
use_kernels: true
|
| 371 |
+
|
| 372 |
+
validators: null
|
| 373 |
+
|
| 374 |
+
embedder_args:
|
| 375 |
+
atom_encoder_depth: 3
|
| 376 |
+
atom_encoder_heads: 4
|
| 377 |
+
add_mol_type_feat: true
|
| 378 |
+
add_method_conditioning: true
|
| 379 |
+
add_modified_flag: true
|
| 380 |
+
add_cyclic_flag: true
|
| 381 |
+
|
| 382 |
+
msa_args:
|
| 383 |
+
msa_s: 64
|
| 384 |
+
msa_blocks: 4
|
| 385 |
+
msa_dropout: 0.15
|
| 386 |
+
z_dropout: 0.25
|
| 387 |
+
miniformer_blocks: false
|
| 388 |
+
pairwise_head_width: 32
|
| 389 |
+
pairwise_num_heads: 4
|
| 390 |
+
use_paired_feature: true
|
| 391 |
+
activation_checkpointing: false
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
template_args:
|
| 395 |
+
template_dim: 64
|
| 396 |
+
template_blocks: 2
|
| 397 |
+
activation_checkpointing: false
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
pairformer_args:
|
| 401 |
+
num_blocks: 64
|
| 402 |
+
num_heads: 16
|
| 403 |
+
dropout: 0.25
|
| 404 |
+
post_layer_norm: false
|
| 405 |
+
activation_checkpointing: false
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
score_model_args:
|
| 409 |
+
sigma_data: 16
|
| 410 |
+
dim_fourier: 256
|
| 411 |
+
atom_encoder_depth: 3
|
| 412 |
+
atom_encoder_heads: 4
|
| 413 |
+
token_transformer_depth: 24
|
| 414 |
+
token_transformer_heads: 16
|
| 415 |
+
atom_decoder_depth: 3
|
| 416 |
+
atom_decoder_heads: 4
|
| 417 |
+
conditioning_transition_layers: 2
|
| 418 |
+
transformer_post_ln: false
|
| 419 |
+
activation_checkpointing: false
|
| 420 |
+
|
| 421 |
+
confidence_prediction: true
|
| 422 |
+
affinity_prediction: false
|
| 423 |
+
structure_prediction_training: true
|
| 424 |
+
affinity_model_args:
|
| 425 |
+
num_dist_bins: 64
|
| 426 |
+
max_dist: 22
|
| 427 |
+
no_trunk_feats: false
|
| 428 |
+
add_s_to_z_prod: false
|
| 429 |
+
add_s_input_to_s: false
|
| 430 |
+
confidence_args:
|
| 431 |
+
num_plddt_bins: 50
|
| 432 |
+
num_pde_bins: 64
|
| 433 |
+
num_pae_bins: 64
|
| 434 |
+
|
| 435 |
+
training_args:
|
| 436 |
+
recycling_steps: 3
|
| 437 |
+
sampling_steps: 20
|
| 438 |
+
diffusion_multiplicity: 48
|
| 439 |
+
diffusion_samples: 1
|
| 440 |
+
affinity_loss_weight: 3e-3
|
| 441 |
+
confidence_loss_weight: 1e-4
|
| 442 |
+
diffusion_loss_weight: 4.0
|
| 443 |
+
distogram_loss_weight: 3e-2
|
| 444 |
+
bfactor_loss_weight: 1e-3
|
| 445 |
+
adam_beta_1: 0.9
|
| 446 |
+
adam_beta_2: 0.95
|
| 447 |
+
adam_eps: 0.00000001
|
| 448 |
+
lr_scheduler: af3
|
| 449 |
+
base_lr: 0.0
|
| 450 |
+
max_lr: 0.001
|
| 451 |
+
lr_warmup_no_steps: 1000
|
| 452 |
+
lr_start_decay_after_n_steps: 50000
|
| 453 |
+
lr_decay_every_n_steps: 50000
|
| 454 |
+
lr_decay_factor: 0.95
|
| 455 |
+
weight_decay: 0.003
|
| 456 |
+
weight_decay_exclude: true
|
| 457 |
+
|
| 458 |
+
validation_args:
|
| 459 |
+
recycling_steps: 3
|
| 460 |
+
sampling_steps: 200
|
| 461 |
+
diffusion_samples: 5
|
| 462 |
+
symmetry_correction: false
|
| 463 |
+
|
| 464 |
+
diffusion_process_args:
|
| 465 |
+
sigma_min: 0.0004 # min noise level
|
| 466 |
+
sigma_max: 160.0 # max noise level
|
| 467 |
+
sigma_data: 16.0 # standard deviation of data distribution
|
| 468 |
+
rho: 7 # controls the sampling schedule
|
| 469 |
+
P_mean: -1.2 # mean of log-normal distribution from which noise is drawn for training
|
| 470 |
+
P_std: 1.5 # standard deviation of log-normal distribution from which noise is drawn for training
|
| 471 |
+
gamma_0: 0.8
|
| 472 |
+
gamma_min: 1.0
|
| 473 |
+
noise_scale: 1.0
|
| 474 |
+
step_scale: 1.0
|
| 475 |
+
mse_rotational_alignment: true
|
| 476 |
+
coordinate_augmentation: true
|
| 477 |
+
alignment_reverse_diff: true
|
| 478 |
+
synchronize_sigmas: false
|
| 479 |
+
|
| 480 |
+
diffusion_loss_args:
|
| 481 |
+
add_smooth_lddt_loss: true
|
| 482 |
+
add_bond_loss: false
|
| 483 |
+
nucleotide_loss_weight: 5.0
|
| 484 |
+
ligand_loss_weight: 10.0
|
model/boltzgen/resources/config/train/boltzgen.yaml
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: boltzgen.task.train.train.Training
|
| 2 |
+
|
| 3 |
+
trainer:
|
| 4 |
+
accelerator: gpu
|
| 5 |
+
devices: 8
|
| 6 |
+
precision: bf16-mixed
|
| 7 |
+
gradient_clip_val: 10.0
|
| 8 |
+
accumulate_grad_batches: 1
|
| 9 |
+
max_epochs: -1
|
| 10 |
+
num_sanity_val_steps: 3
|
| 11 |
+
log_every_n_steps: 1
|
| 12 |
+
|
| 13 |
+
wandb:
|
| 14 |
+
group: boltzgen
|
| 15 |
+
project: boltzgen
|
| 16 |
+
entity: yourwandb
|
| 17 |
+
|
| 18 |
+
name: a_big_run_resume3
|
| 19 |
+
slurm: true
|
| 20 |
+
output: workdir
|
| 21 |
+
strict_loading: false
|
| 22 |
+
resume: null
|
| 23 |
+
pretrained: null
|
| 24 |
+
debug: false
|
| 25 |
+
save_every_n_train_steps: 2500
|
| 26 |
+
disable_checkpoint: false
|
| 27 |
+
matmul_precision: null
|
| 28 |
+
save_top_k: -1
|
| 29 |
+
|
| 30 |
+
data:
|
| 31 |
+
datasets:
|
| 32 |
+
- _target_: boltzgen.task.train.data.DatasetConfig
|
| 33 |
+
target_dir: ./training_data/targets
|
| 34 |
+
msa_dir: ./training_data/msa
|
| 35 |
+
prob: 0.6
|
| 36 |
+
filters:
|
| 37 |
+
- _target_: boltzgen.data.filter.dynamic.size.SizeFilter
|
| 38 |
+
min_chains: 1
|
| 39 |
+
max_chains: 300
|
| 40 |
+
- _target_: boltzgen.data.filter.dynamic.date.DateFilter
|
| 41 |
+
date: "2023-06-01"
|
| 42 |
+
ref: released
|
| 43 |
+
- _target_: boltzgen.data.filter.dynamic.resolution.ResolutionFilter
|
| 44 |
+
resolution: 9.0
|
| 45 |
+
sampler:
|
| 46 |
+
_target_: boltzgen.data.sample.cluster.ClusterSampler
|
| 47 |
+
cropper:
|
| 48 |
+
_target_: boltzgen.data.crop.multimer.MultimerCropper
|
| 49 |
+
neighborhood_sizes: [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 ]
|
| 50 |
+
split: ./model/boltzgen/resources/splits/validation_ids_boltz2_all.txt
|
| 51 |
+
symmetry_correction: false
|
| 52 |
+
val_group: "RCSB"
|
| 53 |
+
|
| 54 |
+
# AFDB Distillation Data
|
| 55 |
+
- _target_: boltzgen.task.train.data.DatasetConfig
|
| 56 |
+
manifest_path: ./training_data/afdb/afdb_manifest_foldseek_c75_confidence.json
|
| 57 |
+
target_dir: ./training_data/afdb/targets
|
| 58 |
+
msa_dir: ./training_data/afdb/msa
|
| 59 |
+
prob: 0.3
|
| 60 |
+
filters:
|
| 61 |
+
- _target_: boltzgen.data.filter.dynamic.size.SizeFilter
|
| 62 |
+
min_chains: 1
|
| 63 |
+
max_chains: 300
|
| 64 |
+
- _target_: boltzgen.data.filter.dynamic.confidence.ConfidenceFilter
|
| 65 |
+
composition_op: "AND"
|
| 66 |
+
metrics: ["confidence_score"]
|
| 67 |
+
compare_ops: ["greater"]
|
| 68 |
+
thresholds: [70]
|
| 69 |
+
sampler:
|
| 70 |
+
_target_: boltzgen.data.sample.cluster.ClusterSampler
|
| 71 |
+
cropper:
|
| 72 |
+
_target_: boltzgen.data.crop.multimer.MultimerCropper
|
| 73 |
+
neighborhood_sizes: [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 ]
|
| 74 |
+
symmetry_correction: true
|
| 75 |
+
override_method: "AFDB"
|
| 76 |
+
override_bfactor: true
|
| 77 |
+
|
| 78 |
+
# Protein-Ligand Distillation Data
|
| 79 |
+
- _target_: boltzgen.task.train.data.DatasetConfig
|
| 80 |
+
target_dir: ./training_data/protein_ligand/targets
|
| 81 |
+
msa_dir: ./training_data/protein_ligand/msa
|
| 82 |
+
moldir: ./training_data/protein_ligand/mols
|
| 83 |
+
prob: 0.03
|
| 84 |
+
filters:
|
| 85 |
+
- _target_: boltzgen.data.filter.dynamic.size.SizeFilter
|
| 86 |
+
min_chains: 1
|
| 87 |
+
max_chains: 300
|
| 88 |
+
- _target_: boltzgen.data.filter.dynamic.confidence.ConfidenceFilter
|
| 89 |
+
composition_op: "AND"
|
| 90 |
+
metrics: ["complex_ipde", "complex_pde", "iptm"]
|
| 91 |
+
compare_ops: ["lesser", "lesser", "greater"]
|
| 92 |
+
thresholds: [1.5, 1.5, 0.9]
|
| 93 |
+
sampler:
|
| 94 |
+
_target_: boltzgen.data.sample.cluster.ClusterSampler
|
| 95 |
+
beta_chain: 0.05
|
| 96 |
+
cropper:
|
| 97 |
+
_target_: boltzgen.data.crop.multimer.MultimerCropper
|
| 98 |
+
neighborhood_sizes: [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 ]
|
| 99 |
+
symmetry_correction: true
|
| 100 |
+
override_method: "BOLTZ-1"
|
| 101 |
+
|
| 102 |
+
# RNA Distillation Data
|
| 103 |
+
- _target_: boltzgen.task.train.data.DatasetConfig
|
| 104 |
+
target_dir: ./training_data/rna/targets
|
| 105 |
+
msa_dir: ./training_data/rna/msa
|
| 106 |
+
prob: 0.04
|
| 107 |
+
filters:
|
| 108 |
+
- _target_: boltzgen.data.filter.dynamic.size.SizeFilter
|
| 109 |
+
min_chains: 1
|
| 110 |
+
max_chains: 300
|
| 111 |
+
- _target_: boltzgen.data.filter.dynamic.confidence.ConfidenceFilter
|
| 112 |
+
composition_op: "OR"
|
| 113 |
+
metrics: ["complex_pde"]
|
| 114 |
+
compare_ops: ["lesser"]
|
| 115 |
+
thresholds: [2.0]
|
| 116 |
+
sampler:
|
| 117 |
+
_target_: boltzgen.data.sample.cluster.ClusterSampler
|
| 118 |
+
cropper:
|
| 119 |
+
_target_: boltzgen.data.crop.multimer.MultimerCropper
|
| 120 |
+
neighborhood_sizes: [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 ]
|
| 121 |
+
symmetry_correction: true
|
| 122 |
+
override_method: "BOLTZ-1"
|
| 123 |
+
|
| 124 |
+
# Protein-DNA Distillation Data
|
| 125 |
+
- _target_: boltzgen.task.train.data.DatasetConfig
|
| 126 |
+
target_dir: ./training_data/protein_dna/targets
|
| 127 |
+
msa_dir: ./training_data/protein_dna/msa
|
| 128 |
+
prob: 0.03
|
| 129 |
+
filters:
|
| 130 |
+
- _target_: boltzgen.data.filter.dynamic.size.SizeFilter
|
| 131 |
+
min_chains: 1
|
| 132 |
+
max_chains: 300
|
| 133 |
+
- _target_: boltzgen.data.filter.dynamic.confidence.ConfidenceFilter
|
| 134 |
+
composition_op: "AND"
|
| 135 |
+
metrics: ["complex_ipde", "complex_pde", "iptm"]
|
| 136 |
+
compare_ops: ["lesser", "lesser", "greater"]
|
| 137 |
+
thresholds: [1.0, 2.0, 0.7]
|
| 138 |
+
sampler:
|
| 139 |
+
_target_: boltzgen.data.sample.cluster.ClusterSampler
|
| 140 |
+
beta_chain: 0.05
|
| 141 |
+
cropper:
|
| 142 |
+
_target_: boltzgen.data.crop.multimer.MultimerCropper
|
| 143 |
+
neighborhood_sizes: [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 ]
|
| 144 |
+
symmetry_correction: true
|
| 145 |
+
override_method: "BOLTZ-1"
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
tokenizer:
|
| 149 |
+
_target_: boltzgen.data.tokenize.tokenizer.Tokenizer
|
| 150 |
+
atomize_modified_residues: false
|
| 151 |
+
featurizer:
|
| 152 |
+
_target_: boltzgen.data.feature.featurizer.Featurizer
|
| 153 |
+
moldir: ./training_data/mols
|
| 154 |
+
max_tokens: 512
|
| 155 |
+
max_atoms: 5120
|
| 156 |
+
max_seqs: 4096
|
| 157 |
+
pad_to_max_tokens: true
|
| 158 |
+
pad_to_max_atoms: true
|
| 159 |
+
pad_to_max_seqs: true
|
| 160 |
+
samples_per_epoch: 100000
|
| 161 |
+
batch_size: 1
|
| 162 |
+
num_workers: 2
|
| 163 |
+
random_seed: 42
|
| 164 |
+
pin_memory: false
|
| 165 |
+
overfit: null
|
| 166 |
+
return_train_symmetries: false
|
| 167 |
+
return_val_symmetries: false
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
atoms_per_window_queries: 32
|
| 171 |
+
min_dist: 2.0
|
| 172 |
+
max_dist: 22.0
|
| 173 |
+
num_bins: 64
|
| 174 |
+
single_sequence_prop_training: 0.1
|
| 175 |
+
msa_sampling_training: true
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
# Design
|
| 179 |
+
design: true
|
| 180 |
+
backbone_only: false
|
| 181 |
+
atom14: true
|
| 182 |
+
atom37: false
|
| 183 |
+
selector:
|
| 184 |
+
_target_: boltzgen.data.select.protein.ProteinSelector
|
| 185 |
+
design_neighborhood_sizes: [2, 4, 6,8,10,12,14,16,18]
|
| 186 |
+
substructure_neighborhood_sizes: [2,4,6,8,10,12,24]
|
| 187 |
+
structure_condition_prob: 0.4
|
| 188 |
+
distance_noise_std: 1
|
| 189 |
+
run_selection: true
|
| 190 |
+
specify_binding_sites: true
|
| 191 |
+
ss_condition_prob: 0.1
|
| 192 |
+
select_all: false
|
| 193 |
+
|
| 194 |
+
# Design datasets
|
| 195 |
+
monomer_split: ./model/boltzgen/resources/splits/val_monomers_boltzgen_min50_max220.txt
|
| 196 |
+
monomer_target_dir: ./training_data/targets
|
| 197 |
+
monomer_target_structure_condition: true
|
| 198 |
+
monomer_seq_len: 100
|
| 199 |
+
|
| 200 |
+
ligand_split: ./model/boltzgen/resources/splits/val_ccd_pdb_pairs_boltzgen.txt
|
| 201 |
+
ligand_target_dir: ./training_data/targets
|
| 202 |
+
ligand_seq_len: 100
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
model:
|
| 206 |
+
_target_: boltzgen.model.models.boltz.Boltz
|
| 207 |
+
atom_s: 128
|
| 208 |
+
atom_z: 16
|
| 209 |
+
token_s: 384
|
| 210 |
+
token_z: 128
|
| 211 |
+
num_bins: 64
|
| 212 |
+
atom_feature_dim: 388
|
| 213 |
+
atoms_per_window_queries: 32
|
| 214 |
+
atoms_per_window_keys: 128
|
| 215 |
+
use_miniformer: false
|
| 216 |
+
ema: true
|
| 217 |
+
ema_decay: 0.999
|
| 218 |
+
exclude_ions_from_lddt: true
|
| 219 |
+
num_val_datasets: 1 # New
|
| 220 |
+
ignore_ckpt_shape_mismatch: false # New
|
| 221 |
+
aggregate_distogram: true # New
|
| 222 |
+
bond_type_feature: true
|
| 223 |
+
predict_bfactor: true
|
| 224 |
+
checkpoint_diffusion_conditioning: true
|
| 225 |
+
use_kernels: true
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
validators:
|
| 229 |
+
- _target_: boltzgen.model.validation.design.DesignValidator
|
| 230 |
+
val_names: ["RCSB"]
|
| 231 |
+
confidence_prediction: ${model.confidence_prediction}
|
| 232 |
+
atom14: ${data.atom14}
|
| 233 |
+
atom37: ${data.atom37}
|
| 234 |
+
|
| 235 |
+
masker_args:
|
| 236 |
+
mask: true
|
| 237 |
+
mask_backbone: false
|
| 238 |
+
mask_disto: true
|
| 239 |
+
|
| 240 |
+
embedder_args:
|
| 241 |
+
atom_encoder_depth: 3
|
| 242 |
+
atom_encoder_heads: 4
|
| 243 |
+
add_mol_type_feat: true
|
| 244 |
+
add_method_conditioning: true
|
| 245 |
+
add_modified_flag: true
|
| 246 |
+
add_cyclic_flag: true
|
| 247 |
+
add_design_mask_flag: true
|
| 248 |
+
add_binding_specification: true
|
| 249 |
+
add_ss_specification: true
|
| 250 |
+
|
| 251 |
+
freeze_template_weights: true
|
| 252 |
+
use_templates: true
|
| 253 |
+
template_args:
|
| 254 |
+
template_dim: 64
|
| 255 |
+
template_blocks: 2
|
| 256 |
+
activation_checkpointing: false
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
use_token_distances: true
|
| 260 |
+
token_distance_args:
|
| 261 |
+
token_distance_dim: 64
|
| 262 |
+
token_distance_blocks: 2
|
| 263 |
+
use_token_distance_feats: true
|
| 264 |
+
distance_gaussian_dim: 32
|
| 265 |
+
activation_checkpointing: true
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
msa_args:
|
| 269 |
+
msa_s: 64
|
| 270 |
+
msa_blocks: 4
|
| 271 |
+
msa_dropout: 0.15
|
| 272 |
+
z_dropout: 0.25
|
| 273 |
+
miniformer_blocks: false
|
| 274 |
+
pairwise_head_width: 32
|
| 275 |
+
pairwise_num_heads: 4
|
| 276 |
+
use_paired_feature: true
|
| 277 |
+
activation_checkpointing: true
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
pairformer_args:
|
| 281 |
+
num_blocks: 64
|
| 282 |
+
num_heads: 16
|
| 283 |
+
dropout: 0.25
|
| 284 |
+
post_layer_norm: false
|
| 285 |
+
activation_checkpointing: true
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
score_model_args:
|
| 289 |
+
sigma_data: 16
|
| 290 |
+
dim_fourier: 256
|
| 291 |
+
atom_encoder_depth: 3
|
| 292 |
+
atom_encoder_heads: 4
|
| 293 |
+
|
| 294 |
+
# token level args
|
| 295 |
+
token_layers: 1
|
| 296 |
+
token_transformer_depth: 24
|
| 297 |
+
token_transformer_heads: 16
|
| 298 |
+
diffusion_pairformer_args:
|
| 299 |
+
num_blocks: 0
|
| 300 |
+
num_heads: 2
|
| 301 |
+
dropout: 0
|
| 302 |
+
use_s_to_z: false
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
atom_decoder_depth: 3
|
| 307 |
+
atom_decoder_heads: 4
|
| 308 |
+
conditioning_transition_layers: 2
|
| 309 |
+
transformer_post_ln: false
|
| 310 |
+
activation_checkpointing: true
|
| 311 |
+
|
| 312 |
+
confidence_prediction: false
|
| 313 |
+
structure_prediction_training: true
|
| 314 |
+
|
| 315 |
+
training_args:
|
| 316 |
+
recycling_steps: 3
|
| 317 |
+
sampling_steps: 20
|
| 318 |
+
diffusion_multiplicity: 32
|
| 319 |
+
diffusion_samples: 1
|
| 320 |
+
confidence_loss_weight: 1e-4
|
| 321 |
+
diffusion_loss_weight: 4.0
|
| 322 |
+
distogram_loss_weight: 3e-2
|
| 323 |
+
bfactor_loss_weight: 1e-3
|
| 324 |
+
adam_beta_1: 0.9
|
| 325 |
+
adam_beta_2: 0.95
|
| 326 |
+
adam_eps: 0.00000001
|
| 327 |
+
lr_scheduler: af3
|
| 328 |
+
base_lr: 0.0
|
| 329 |
+
max_lr: 0.0005
|
| 330 |
+
lr_warmup_no_steps: 1000
|
| 331 |
+
lr_start_decay_after_n_steps: 50000
|
| 332 |
+
lr_decay_every_n_steps: 50000
|
| 333 |
+
lr_decay_factor: 0.95
|
| 334 |
+
weight_decay: 0.003
|
| 335 |
+
weight_decay_exclude: true
|
| 336 |
+
|
| 337 |
+
validation_args:
|
| 338 |
+
recycling_steps: 3
|
| 339 |
+
sampling_steps: 200
|
| 340 |
+
diffusion_samples: 1
|
| 341 |
+
symmetry_correction: false
|
| 342 |
+
|
| 343 |
+
diffusion_process_args:
|
| 344 |
+
sigma_min: 0.0004 # min noise level
|
| 345 |
+
sigma_max: 160.0 # max noise level
|
| 346 |
+
sigma_data: 16.0 # standard deviation of data distribution
|
| 347 |
+
rho: 7 # controls the sampling schedule
|
| 348 |
+
P_mean: -1.2 # mean of log-normal distribution from which noise is drawn for training
|
| 349 |
+
P_std: 1.5 # standard deviation of log-normal distribution from which noise is drawn for training
|
| 350 |
+
gamma_0: 0.8
|
| 351 |
+
gamma_min: 1.0
|
| 352 |
+
noise_scale: 1.0
|
| 353 |
+
step_scale: 1.0
|
| 354 |
+
mse_rotational_alignment: true
|
| 355 |
+
coordinate_augmentation: true
|
| 356 |
+
alignment_reverse_diff: true
|
| 357 |
+
synchronize_sigmas: false
|
| 358 |
+
|
| 359 |
+
diffusion_loss_args:
|
| 360 |
+
add_smooth_lddt_loss: true
|
| 361 |
+
add_bond_loss: false
|
| 362 |
+
nucleotide_loss_weight: 5.0
|
| 363 |
+
ligand_loss_weight: 10.0
|
| 364 |
+
|
| 365 |
+
refolding_validator:
|
| 366 |
+
_target_: boltzgen.model.validation.refolding.RefoldingValidator
|
| 367 |
+
val_names: ["RCSB"]
|
| 368 |
+
step_scale: 1.5
|
| 369 |
+
noise_scale: 0.75
|
| 370 |
+
atom14: ${data.atom14}
|
| 371 |
+
atom37: ${data.atom37}
|
| 372 |
+
val_monomer: ${data.monomer_split}
|
| 373 |
+
val_ligand: ${data.ligand_split}
|
| 374 |
+
analyze_task:
|
| 375 |
+
_target_: boltzgen.task.analyze.analyze.Analyze
|
| 376 |
+
name: ${name}
|
| 377 |
+
debug: ${debug}
|
| 378 |
+
design_dir: null
|
| 379 |
+
num_processes: 1
|
| 380 |
+
|
| 381 |
+
# Common metrics to compute
|
| 382 |
+
affinity_metrics: false
|
| 383 |
+
allatom_fold_metrics: true
|
| 384 |
+
backbone_fold_metrics: true
|
| 385 |
+
noncovalents_original: false
|
| 386 |
+
noncovalents_refolded: false
|
| 387 |
+
delta_sasa_original: false
|
| 388 |
+
delta_sasa_refolded: false
|
| 389 |
+
largest_hydrophobic: false
|
| 390 |
+
largest_hydrophobic_refolded: false
|
| 391 |
+
run_clustering: false
|
| 392 |
+
|
| 393 |
+
# Liability analysis
|
| 394 |
+
liability_analysis: false
|
| 395 |
+
liability_modality: peptide
|
| 396 |
+
liability_peptide_type: linear
|
| 397 |
+
|
| 398 |
+
# Uncommon metrics
|
| 399 |
+
diversity_original: true
|
| 400 |
+
diversity_refolded: true
|
| 401 |
+
diversity_per_target_original: false
|
| 402 |
+
diversity_per_target_refolded: false
|
| 403 |
+
novelty_original: false
|
| 404 |
+
novelty_refolded: false
|
| 405 |
+
novelty_per_target_original: false
|
| 406 |
+
novelty_per_target_refolded: false
|
| 407 |
+
|
| 408 |
+
wandb: null
|
| 409 |
+
|
| 410 |
+
data:
|
| 411 |
+
_target_: boltzgen.task.predict.data_from_generated.FromGeneratedDataModule
|
| 412 |
+
cfg:
|
| 413 |
+
_target_: boltzgen.task.predict.data_from_generated.DataConfig
|
| 414 |
+
tokenizer:
|
| 415 |
+
_target_: boltzgen.data.tokenize.tokenizer.Tokenizer
|
| 416 |
+
atomize_modified_residues: false
|
| 417 |
+
featurizer:
|
| 418 |
+
_target_: boltzgen.data.feature.featurizer.Featurizer
|
| 419 |
+
|
| 420 |
+
suffix: .cif
|
| 421 |
+
suffix_metadata: .npz
|
| 422 |
+
suffix_native: _native.cif
|
| 423 |
+
samples_per_target: 1
|
| 424 |
+
num_targets: 100000000
|
| 425 |
+
moldir: ./training_data/mols
|
| 426 |
+
|
| 427 |
+
batch_size: 1
|
| 428 |
+
num_workers: 4
|
| 429 |
+
pin_memory: true
|
| 430 |
+
return_native: true
|
| 431 |
+
|
| 432 |
+
folding_checkpoint: ./training_data/boltz2_fold.ckpt
|
| 433 |
+
|
| 434 |
+
folding_args:
|
| 435 |
+
recycling_steps: 3
|
| 436 |
+
sampling_steps: 200
|
| 437 |
+
diffusion_samples: 1
|
| 438 |
+
|
| 439 |
+
folding_model_args:
|
| 440 |
+
atom_s: 128
|
| 441 |
+
atom_z: 16
|
| 442 |
+
token_s: 384
|
| 443 |
+
token_z: 128
|
| 444 |
+
num_bins: 64
|
| 445 |
+
atom_feature_dim: 388
|
| 446 |
+
atoms_per_window_queries: 32
|
| 447 |
+
atoms_per_window_keys: 128
|
| 448 |
+
compile_pairformer: false
|
| 449 |
+
compile_templates: false
|
| 450 |
+
compile_msa: false
|
| 451 |
+
use_miniformer: false
|
| 452 |
+
ema: true
|
| 453 |
+
ema_decay: 0.999
|
| 454 |
+
exclude_ions_from_lddt: true
|
| 455 |
+
num_val_datasets: 4
|
| 456 |
+
ignore_ckpt_shape_mismatch: false
|
| 457 |
+
aggregate_distogram: true
|
| 458 |
+
bond_type_feature: true
|
| 459 |
+
conditioning_cutoff_min: 4.0
|
| 460 |
+
conditioning_cutoff_max: 20.0
|
| 461 |
+
use_templates: true
|
| 462 |
+
predict_bfactor: true
|
| 463 |
+
checkpoint_diffusion_conditioning: false
|
| 464 |
+
use_kernels: true
|
| 465 |
+
|
| 466 |
+
validators: null
|
| 467 |
+
|
| 468 |
+
embedder_args:
|
| 469 |
+
atom_encoder_depth: 3
|
| 470 |
+
atom_encoder_heads: 4
|
| 471 |
+
add_mol_type_feat: true
|
| 472 |
+
add_method_conditioning: true
|
| 473 |
+
add_modified_flag: true
|
| 474 |
+
add_cyclic_flag: true
|
| 475 |
+
|
| 476 |
+
msa_args:
|
| 477 |
+
msa_s: 64
|
| 478 |
+
msa_blocks: 4
|
| 479 |
+
msa_dropout: 0.15
|
| 480 |
+
z_dropout: 0.25
|
| 481 |
+
miniformer_blocks: false
|
| 482 |
+
pairwise_head_width: 32
|
| 483 |
+
pairwise_num_heads: 4
|
| 484 |
+
use_paired_feature: true
|
| 485 |
+
activation_checkpointing: false
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
template_args:
|
| 489 |
+
template_dim: 64
|
| 490 |
+
template_blocks: 2
|
| 491 |
+
activation_checkpointing: false
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
pairformer_args:
|
| 495 |
+
num_blocks: 64
|
| 496 |
+
num_heads: 16
|
| 497 |
+
dropout: 0.25
|
| 498 |
+
post_layer_norm: false
|
| 499 |
+
activation_checkpointing: false
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
score_model_args:
|
| 503 |
+
sigma_data: 16
|
| 504 |
+
dim_fourier: 256
|
| 505 |
+
atom_encoder_depth: 3
|
| 506 |
+
atom_encoder_heads: 4
|
| 507 |
+
token_transformer_depth: 24
|
| 508 |
+
token_transformer_heads: 16
|
| 509 |
+
atom_decoder_depth: 3
|
| 510 |
+
atom_decoder_heads: 4
|
| 511 |
+
conditioning_transition_layers: 2
|
| 512 |
+
transformer_post_ln: false
|
| 513 |
+
activation_checkpointing: false
|
| 514 |
+
|
| 515 |
+
confidence_prediction: false
|
| 516 |
+
affinity_prediction: false
|
| 517 |
+
structure_prediction_training: true
|
| 518 |
+
affinity_model_args:
|
| 519 |
+
num_dist_bins: 64
|
| 520 |
+
max_dist: 22
|
| 521 |
+
no_trunk_feats: false
|
| 522 |
+
add_s_to_z_prod: false
|
| 523 |
+
add_s_input_to_s: false
|
| 524 |
+
confidence_args:
|
| 525 |
+
num_plddt_bins: 50
|
| 526 |
+
num_pde_bins: 64
|
| 527 |
+
num_pae_bins: 64
|
| 528 |
+
|
| 529 |
+
training_args:
|
| 530 |
+
recycling_steps: 3
|
| 531 |
+
sampling_steps: 20
|
| 532 |
+
diffusion_multiplicity: 48
|
| 533 |
+
diffusion_samples: 1
|
| 534 |
+
affinity_loss_weight: 3e-3
|
| 535 |
+
confidence_loss_weight: 1e-4
|
| 536 |
+
diffusion_loss_weight: 4.0
|
| 537 |
+
distogram_loss_weight: 3e-2
|
| 538 |
+
bfactor_loss_weight: 1e-3
|
| 539 |
+
adam_beta_1: 0.9
|
| 540 |
+
adam_beta_2: 0.95
|
| 541 |
+
adam_eps: 0.00000001
|
| 542 |
+
lr_scheduler: af3
|
| 543 |
+
base_lr: 0.0
|
| 544 |
+
max_lr: 0.001
|
| 545 |
+
lr_warmup_no_steps: 1000
|
| 546 |
+
lr_start_decay_after_n_steps: 50000
|
| 547 |
+
lr_decay_every_n_steps: 50000
|
| 548 |
+
lr_decay_factor: 0.95
|
| 549 |
+
weight_decay: 0.003
|
| 550 |
+
weight_decay_exclude: true
|
| 551 |
+
|
| 552 |
+
validation_args:
|
| 553 |
+
recycling_steps: 3
|
| 554 |
+
sampling_steps: 200
|
| 555 |
+
diffusion_samples: 5
|
| 556 |
+
symmetry_correction: false
|
| 557 |
+
|
| 558 |
+
diffusion_process_args:
|
| 559 |
+
sigma_min: 0.0004 # min noise level
|
| 560 |
+
sigma_max: 160.0 # max noise level
|
| 561 |
+
sigma_data: 16.0 # standard deviation of data distribution
|
| 562 |
+
rho: 7 # controls the sampling schedule
|
| 563 |
+
P_mean: -1.2 # mean of log-normal distribution from which noise is drawn for training
|
| 564 |
+
P_std: 1.5 # standard deviation of log-normal distribution from which noise is drawn for training
|
| 565 |
+
gamma_0: 0.8
|
| 566 |
+
gamma_min: 1.0
|
| 567 |
+
noise_scale: 1.0
|
| 568 |
+
step_scale: 1.0
|
| 569 |
+
mse_rotational_alignment: true
|
| 570 |
+
coordinate_augmentation: true
|
| 571 |
+
alignment_reverse_diff: true
|
| 572 |
+
synchronize_sigmas: false
|
| 573 |
+
|
| 574 |
+
diffusion_loss_args:
|
| 575 |
+
add_smooth_lddt_loss: true
|
| 576 |
+
add_bond_loss: false
|
| 577 |
+
nucleotide_loss_weight: 5.0
|
| 578 |
+
ligand_loss_weight: 10.0
|
model/boltzgen/resources/config/train/boltzgen_small.yaml
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: boltzgen.task.train.train.Training
|
| 2 |
+
|
| 3 |
+
trainer:
|
| 4 |
+
accelerator: gpu
|
| 5 |
+
devices: 8
|
| 6 |
+
precision: bf16-mixed
|
| 7 |
+
gradient_clip_val: 10.0
|
| 8 |
+
accumulate_grad_batches: 16
|
| 9 |
+
max_epochs: -1
|
| 10 |
+
num_sanity_val_steps: 3
|
| 11 |
+
log_every_n_steps: 1
|
| 12 |
+
|
| 13 |
+
wandb:
|
| 14 |
+
group: boltzgen
|
| 15 |
+
project: boltzgen
|
| 16 |
+
entity: yourwandb
|
| 17 |
+
|
| 18 |
+
name: small
|
| 19 |
+
output: workdir
|
| 20 |
+
strict_loading: false
|
| 21 |
+
resume: null
|
| 22 |
+
pretrained: ./training_data/boltzgen1_structuretrained_small.ckpt
|
| 23 |
+
debug: false
|
| 24 |
+
save_every_n_train_steps: 2500
|
| 25 |
+
disable_checkpoint: false
|
| 26 |
+
matmul_precision: null
|
| 27 |
+
save_top_k: -1
|
| 28 |
+
# ddp_timeout_seconds: 1000
|
| 29 |
+
|
| 30 |
+
data:
|
| 31 |
+
datasets:
|
| 32 |
+
- _target_: boltzgen.task.train.data.DatasetConfig
|
| 33 |
+
target_dir: ./training_data/targets
|
| 34 |
+
msa_dir: ./training_data/msa
|
| 35 |
+
prob: 1
|
| 36 |
+
filters:
|
| 37 |
+
- _target_: boltzgen.data.filter.dynamic.size.SizeFilter
|
| 38 |
+
min_chains: 1
|
| 39 |
+
max_chains: 300
|
| 40 |
+
- _target_: boltzgen.data.filter.dynamic.date.DateFilter
|
| 41 |
+
date: "2023-06-01"
|
| 42 |
+
ref: released
|
| 43 |
+
- _target_: boltzgen.data.filter.dynamic.resolution.ResolutionFilter
|
| 44 |
+
resolution: 9.0
|
| 45 |
+
sampler:
|
| 46 |
+
_target_: boltzgen.data.sample.cluster.ClusterSampler
|
| 47 |
+
cropper:
|
| 48 |
+
_target_: boltzgen.data.crop.multimer.MultimerCropper
|
| 49 |
+
neighborhood_sizes: [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 ]
|
| 50 |
+
split: ./model/boltzgen/resources/splits/validation_ids_boltz2_all.txt
|
| 51 |
+
symmetry_correction: false
|
| 52 |
+
val_group: "RCSB"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
tokenizer:
|
| 56 |
+
_target_: boltzgen.data.tokenize.tokenizer.Tokenizer
|
| 57 |
+
atomize_modified_residues: false
|
| 58 |
+
featurizer:
|
| 59 |
+
_target_: boltzgen.data.feature.featurizer.Featurizer
|
| 60 |
+
moldir: ./training_data/mols
|
| 61 |
+
max_tokens: 256
|
| 62 |
+
max_atoms: 2048
|
| 63 |
+
max_seqs: 1024
|
| 64 |
+
pad_to_max_tokens: true
|
| 65 |
+
pad_to_max_atoms: true
|
| 66 |
+
pad_to_max_seqs: true
|
| 67 |
+
samples_per_epoch: 100000
|
| 68 |
+
batch_size: 1
|
| 69 |
+
num_workers: 4
|
| 70 |
+
random_seed: 42
|
| 71 |
+
pin_memory: true
|
| 72 |
+
overfit: null
|
| 73 |
+
return_train_symmetries: false
|
| 74 |
+
return_val_symmetries: false
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
atoms_per_window_queries: 32
|
| 78 |
+
min_dist: 2.0
|
| 79 |
+
max_dist: 22.0
|
| 80 |
+
num_bins: 64
|
| 81 |
+
single_sequence_prop_training: 0.1
|
| 82 |
+
msa_sampling_training: true
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# Design
|
| 86 |
+
design: true
|
| 87 |
+
backbone_only: false
|
| 88 |
+
atom14: true
|
| 89 |
+
atom37: false
|
| 90 |
+
selector:
|
| 91 |
+
_target_: boltzgen.data.select.protein.ProteinSelector
|
| 92 |
+
design_neighborhood_sizes: [2, 4, 6,8,10,12,14,16,18]
|
| 93 |
+
substructure_neighborhood_sizes: [2,4,6,8,10,12,24]
|
| 94 |
+
structure_condition_prob: 0.4
|
| 95 |
+
distance_noise_std: 1
|
| 96 |
+
run_selection: true
|
| 97 |
+
specify_binding_sites: true
|
| 98 |
+
ss_condition_prob: 0.1
|
| 99 |
+
select_all: false
|
| 100 |
+
chain_reindexing: false
|
| 101 |
+
|
| 102 |
+
# Design datasets
|
| 103 |
+
monomer_split: ./model/boltzgen/resources/splits/val_monomers_boltzgen_min50_max220.txt
|
| 104 |
+
monomer_target_dir: ./training_data/targets
|
| 105 |
+
monomer_target_structure_condition: true
|
| 106 |
+
monomer_seq_len: 100
|
| 107 |
+
|
| 108 |
+
ligand_split: ./model/boltzgen/resources/splits/val_ccd_pdb_pairs_boltzgen.txt
|
| 109 |
+
ligand_target_dir: ./training_data/targets
|
| 110 |
+
ligand_seq_len: 100
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
model:
|
| 114 |
+
_target_: boltzgen.model.models.boltz.Boltz
|
| 115 |
+
atom_s: 128
|
| 116 |
+
atom_z: 16
|
| 117 |
+
token_s: 384
|
| 118 |
+
token_z: 128
|
| 119 |
+
num_bins: 64
|
| 120 |
+
atom_feature_dim: 388
|
| 121 |
+
atoms_per_window_queries: 32
|
| 122 |
+
atoms_per_window_keys: 128
|
| 123 |
+
use_miniformer: true
|
| 124 |
+
ema: true
|
| 125 |
+
ema_decay: 0.999
|
| 126 |
+
exclude_ions_from_lddt: true
|
| 127 |
+
num_val_datasets: 1 # New
|
| 128 |
+
ignore_ckpt_shape_mismatch: false # New
|
| 129 |
+
aggregate_distogram: true # New
|
| 130 |
+
bond_type_feature: true
|
| 131 |
+
predict_bfactor: true
|
| 132 |
+
predict_res_type: false
|
| 133 |
+
checkpoint_diffusion_conditioning: false
|
| 134 |
+
use_kernels: true
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
validators:
|
| 138 |
+
- _target_: boltzgen.model.validation.design.DesignValidator
|
| 139 |
+
val_names: ["RCSB"]
|
| 140 |
+
confidence_prediction: ${model.confidence_prediction}
|
| 141 |
+
backbone_only: ${data.backbone_only}
|
| 142 |
+
atom14: ${data.atom14}
|
| 143 |
+
atom37: ${data.atom37}
|
| 144 |
+
|
| 145 |
+
masker_args:
|
| 146 |
+
mask: true
|
| 147 |
+
mask_backbone: false
|
| 148 |
+
mask_disto: true
|
| 149 |
+
|
| 150 |
+
embedder_args:
|
| 151 |
+
atom_encoder_depth: 3
|
| 152 |
+
atom_encoder_heads: 4
|
| 153 |
+
add_mol_type_feat: true
|
| 154 |
+
add_method_conditioning: true
|
| 155 |
+
add_modified_flag: true
|
| 156 |
+
add_cyclic_flag: true
|
| 157 |
+
add_design_mask_flag: true
|
| 158 |
+
add_binding_specification: true
|
| 159 |
+
add_ss_specification: true
|
| 160 |
+
|
| 161 |
+
freeze_template_weights: true
|
| 162 |
+
use_templates: true
|
| 163 |
+
template_args:
|
| 164 |
+
template_dim: 64
|
| 165 |
+
template_blocks: 2
|
| 166 |
+
miniformer_blocks: true
|
| 167 |
+
activation_checkpointing: false
|
| 168 |
+
|
| 169 |
+
use_token_distances: true
|
| 170 |
+
token_distance_args:
|
| 171 |
+
token_distance_dim: 64
|
| 172 |
+
token_distance_blocks: 2
|
| 173 |
+
use_token_distance_feats: true
|
| 174 |
+
distance_gaussian_dim: 32
|
| 175 |
+
|
| 176 |
+
msa_args:
|
| 177 |
+
msa_s: 64
|
| 178 |
+
msa_blocks: 3
|
| 179 |
+
msa_dropout: 0.15
|
| 180 |
+
z_dropout: 0.25
|
| 181 |
+
miniformer_blocks: true
|
| 182 |
+
pairwise_head_width: 32
|
| 183 |
+
pairwise_num_heads: 4
|
| 184 |
+
use_paired_feature: true
|
| 185 |
+
activation_checkpointing: false
|
| 186 |
+
|
| 187 |
+
pairformer_args:
|
| 188 |
+
num_blocks: 12
|
| 189 |
+
num_heads: 16
|
| 190 |
+
dropout: 0.25
|
| 191 |
+
post_layer_norm: false
|
| 192 |
+
activation_checkpointing: false
|
| 193 |
+
|
| 194 |
+
score_model_args:
|
| 195 |
+
sigma_data: 16
|
| 196 |
+
dim_fourier: 256
|
| 197 |
+
atom_encoder_depth: 3
|
| 198 |
+
atom_encoder_heads: 4
|
| 199 |
+
|
| 200 |
+
# token level args
|
| 201 |
+
token_layers: 1
|
| 202 |
+
token_transformer_depth: 8
|
| 203 |
+
token_transformer_heads: 16
|
| 204 |
+
diffusion_pairformer_args:
|
| 205 |
+
num_blocks: 0
|
| 206 |
+
num_heads: 2
|
| 207 |
+
dropout: 0
|
| 208 |
+
use_s_to_z: false
|
| 209 |
+
|
| 210 |
+
atom_decoder_depth: 3
|
| 211 |
+
atom_decoder_heads: 4
|
| 212 |
+
conditioning_transition_layers: 2
|
| 213 |
+
transformer_post_ln: false
|
| 214 |
+
activation_checkpointing: false
|
| 215 |
+
|
| 216 |
+
confidence_prediction: false
|
| 217 |
+
structure_prediction_training: true
|
| 218 |
+
training_args:
|
| 219 |
+
recycling_steps: 3
|
| 220 |
+
sampling_steps: 20
|
| 221 |
+
diffusion_multiplicity: 12
|
| 222 |
+
diffusion_samples: 1
|
| 223 |
+
confidence_loss_weight: 1e-4
|
| 224 |
+
diffusion_loss_weight: 4.0
|
| 225 |
+
distogram_loss_weight: 3e-2
|
| 226 |
+
bfactor_loss_weight: 1e-3
|
| 227 |
+
res_type_loss_weight: 3e-2
|
| 228 |
+
adam_beta_1: 0.9
|
| 229 |
+
adam_beta_2: 0.95
|
| 230 |
+
adam_eps: 0.00000001
|
| 231 |
+
lr_scheduler: af3
|
| 232 |
+
base_lr: 0.0
|
| 233 |
+
max_lr: 0.0018
|
| 234 |
+
lr_warmup_no_steps: 1000
|
| 235 |
+
lr_start_decay_after_n_steps: 50000
|
| 236 |
+
lr_decay_every_n_steps: 50000
|
| 237 |
+
lr_decay_factor: 0.95
|
| 238 |
+
weight_decay: 0.003
|
| 239 |
+
weight_decay_exclude: true
|
| 240 |
+
|
| 241 |
+
validation_args:
|
| 242 |
+
recycling_steps: 3
|
| 243 |
+
sampling_steps: 200
|
| 244 |
+
diffusion_samples: 1
|
| 245 |
+
symmetry_correction: false
|
| 246 |
+
|
| 247 |
+
diffusion_process_args:
|
| 248 |
+
sigma_min: 0.0004 # min noise level
|
| 249 |
+
sigma_max: 160.0 # max noise level
|
| 250 |
+
sigma_data: 16.0 # standard deviation of data distribution
|
| 251 |
+
rho: 7 # controls the sampling schedule
|
| 252 |
+
P_mean: -1.2 # mean of log-normal distribution from which noise is drawn for training
|
| 253 |
+
P_std: 1.5 # standard deviation of log-normal distribution from which noise is drawn for training
|
| 254 |
+
gamma_0: 0.8
|
| 255 |
+
gamma_min: 1.0
|
| 256 |
+
noise_scale: 1.0
|
| 257 |
+
step_scale: 1.0
|
| 258 |
+
mse_rotational_alignment: true
|
| 259 |
+
coordinate_augmentation: true
|
| 260 |
+
alignment_reverse_diff: true
|
| 261 |
+
synchronize_sigmas: false
|
| 262 |
+
|
| 263 |
+
diffusion_loss_args:
|
| 264 |
+
add_smooth_lddt_loss: true
|
| 265 |
+
add_bond_loss: false
|
| 266 |
+
nucleotide_loss_weight: 5.0
|
| 267 |
+
ligand_loss_weight: 10.0
|
| 268 |
+
|
| 269 |
+
refolding_validator:
|
| 270 |
+
_target_: boltzgen.model.validation.refolding.RefoldingValidator
|
| 271 |
+
val_names: ["RCSB"]
|
| 272 |
+
step_scale: 1.5
|
| 273 |
+
noise_scale: 0.75
|
| 274 |
+
atom14: ${data.atom14}
|
| 275 |
+
atom37: ${data.atom37}
|
| 276 |
+
backbone_only: ${data.backbone_only}
|
| 277 |
+
val_monomer: ${data.monomer_split}
|
| 278 |
+
val_ligand: ${data.ligand_split}
|
| 279 |
+
analyze_task:
|
| 280 |
+
_target_: boltzgen.task.analyze.analyze.Analyze
|
| 281 |
+
name: ${name}
|
| 282 |
+
debug: ${debug}
|
| 283 |
+
design_dir: null
|
| 284 |
+
num_processes: 1
|
| 285 |
+
|
| 286 |
+
# Common metrics to compute
|
| 287 |
+
affinity_metrics: false
|
| 288 |
+
allatom_fold_metrics: true
|
| 289 |
+
backbone_fold_metrics: true
|
| 290 |
+
noncovalents_original: false
|
| 291 |
+
noncovalents_refolded: false
|
| 292 |
+
delta_sasa_original: false
|
| 293 |
+
delta_sasa_refolded: false
|
| 294 |
+
largest_hydrophobic: false
|
| 295 |
+
largest_hydrophobic_refolded: false
|
| 296 |
+
run_clustering: false
|
| 297 |
+
|
| 298 |
+
# Liability analysis
|
| 299 |
+
liability_analysis: false
|
| 300 |
+
liability_modality: peptide
|
| 301 |
+
liability_peptide_type: linear
|
| 302 |
+
|
| 303 |
+
# Uncommon metrics
|
| 304 |
+
diversity_original: true
|
| 305 |
+
diversity_refolded: true
|
| 306 |
+
diversity_per_target_original: false
|
| 307 |
+
diversity_per_target_refolded: false
|
| 308 |
+
novelty_original: false
|
| 309 |
+
novelty_refolded: false
|
| 310 |
+
novelty_per_target_original: false
|
| 311 |
+
novelty_per_target_refolded: false
|
| 312 |
+
|
| 313 |
+
wandb: null
|
| 314 |
+
|
| 315 |
+
data:
|
| 316 |
+
_target_: boltzgen.task.predict.data_from_generated.FromGeneratedDataModule
|
| 317 |
+
cfg:
|
| 318 |
+
_target_: boltzgen.task.predict.data_from_generated.DataConfig
|
| 319 |
+
tokenizer:
|
| 320 |
+
_target_: boltzgen.data.tokenize.tokenizer.Tokenizer
|
| 321 |
+
atomize_modified_residues: false
|
| 322 |
+
featurizer:
|
| 323 |
+
_target_: boltzgen.data.feature.featurizer.Featurizer
|
| 324 |
+
|
| 325 |
+
suffix: .cif
|
| 326 |
+
suffix_metadata: .npz
|
| 327 |
+
suffix_native: _native.cif
|
| 328 |
+
samples_per_target: 1
|
| 329 |
+
num_targets: 100000000
|
| 330 |
+
moldir: ./training_data/mols
|
| 331 |
+
|
| 332 |
+
batch_size: 1
|
| 333 |
+
num_workers: 4
|
| 334 |
+
pin_memory: true
|
| 335 |
+
target_templates: true
|
| 336 |
+
return_native: true
|
| 337 |
+
|
| 338 |
+
folding_checkpoint: ./training_data/boltz2_fold.ckpt
|
| 339 |
+
|
| 340 |
+
folding_args:
|
| 341 |
+
recycling_steps: 3
|
| 342 |
+
sampling_steps: 200
|
| 343 |
+
diffusion_samples: 1
|
| 344 |
+
|
| 345 |
+
folding_model_args:
|
| 346 |
+
validators: null
|
model/boltzgen/resources/config/train/inverse_folding.yaml
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: boltzgen.task.train.train.Training
|
| 2 |
+
|
| 3 |
+
trainer:
|
| 4 |
+
accelerator: cuda
|
| 5 |
+
devices: 4
|
| 6 |
+
precision: 32
|
| 7 |
+
gradient_clip_val: 10.0
|
| 8 |
+
accumulate_grad_batches: 1
|
| 9 |
+
max_epochs: 5
|
| 10 |
+
num_sanity_val_steps: 1
|
| 11 |
+
log_every_n_steps: 1
|
| 12 |
+
|
| 13 |
+
wandb:
|
| 14 |
+
group: boltzgen
|
| 15 |
+
project: boltzgen
|
| 16 |
+
entity: yourwandb
|
| 17 |
+
|
| 18 |
+
name: if_lr_scheduler
|
| 19 |
+
output: workdir
|
| 20 |
+
strict_loading: false
|
| 21 |
+
resume: null
|
| 22 |
+
debug: false
|
| 23 |
+
save_every_n_train_steps: 2500
|
| 24 |
+
disable_checkpoint: false
|
| 25 |
+
matmul_precision: null
|
| 26 |
+
save_top_k: -1
|
| 27 |
+
|
| 28 |
+
data:
|
| 29 |
+
datasets:
|
| 30 |
+
- _target_: boltzgen.task.train.data.DatasetConfig
|
| 31 |
+
target_dir: ./training_data/targets
|
| 32 |
+
msa_dir: ./training_data/msa
|
| 33 |
+
prob: 1
|
| 34 |
+
filters:
|
| 35 |
+
- _target_: boltzgen.data.filter.dynamic.size.SizeFilter
|
| 36 |
+
min_chains: 1
|
| 37 |
+
max_chains: 300
|
| 38 |
+
- _target_: boltzgen.data.filter.dynamic.date.DateFilter
|
| 39 |
+
date: "2023-06-01"
|
| 40 |
+
ref: released
|
| 41 |
+
- _target_: boltzgen.data.filter.dynamic.resolution.ResolutionFilter
|
| 42 |
+
resolution: 9.0
|
| 43 |
+
- _target_: boltzgen.data.filter.dynamic.min_protein_residues.MinProteinResiduesFilter
|
| 44 |
+
min_residues: 5
|
| 45 |
+
- _target_: boltzgen.data.filter.dynamic.pdb_id_txtfile.FilterIDFromTXT
|
| 46 |
+
paths:
|
| 47 |
+
- data/exclude_ids/fibril.txt
|
| 48 |
+
- data/exclude_ids/transmembrane.txt
|
| 49 |
+
sampler:
|
| 50 |
+
_target_: boltzgen.data.sample.cluster.ClusterSampler
|
| 51 |
+
cropper:
|
| 52 |
+
_target_: boltzgen.data.crop.multimer.MultimerCropper
|
| 53 |
+
neighborhood_sizes: [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 ]
|
| 54 |
+
split: ./model/boltzgen/resources/splits/validation_ids_boltz2_all.txt
|
| 55 |
+
symmetry_correction: false
|
| 56 |
+
val_group: "RCSB"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
tokenizer:
|
| 60 |
+
_target_: boltzgen.data.tokenize.tokenizer.Tokenizer
|
| 61 |
+
atomize_modified_residues: false
|
| 62 |
+
featurizer:
|
| 63 |
+
_target_: boltzgen.data.feature.featurizer.Featurizer
|
| 64 |
+
moldir: ./training_data/mols
|
| 65 |
+
max_tokens: 1024
|
| 66 |
+
max_atoms: 8192
|
| 67 |
+
max_seqs: 1
|
| 68 |
+
pad_to_max_tokens: true
|
| 69 |
+
pad_to_max_atoms: true
|
| 70 |
+
pad_to_max_seqs: true
|
| 71 |
+
samples_per_epoch: 600000
|
| 72 |
+
batch_size: 2
|
| 73 |
+
num_workers: 32
|
| 74 |
+
random_seed: 42
|
| 75 |
+
pin_memory: true
|
| 76 |
+
overfit: null
|
| 77 |
+
return_train_symmetries: false
|
| 78 |
+
return_val_symmetries: false
|
| 79 |
+
compute_frames: false
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
atoms_per_window_queries: 32
|
| 83 |
+
min_dist: 2.0
|
| 84 |
+
max_dist: 22.0
|
| 85 |
+
num_bins: 64
|
| 86 |
+
single_sequence_prop_training: 0.05
|
| 87 |
+
msa_sampling_training: true
|
| 88 |
+
|
| 89 |
+
# Design
|
| 90 |
+
design: true
|
| 91 |
+
backbone_only: true
|
| 92 |
+
atom14: false
|
| 93 |
+
atom37: false
|
| 94 |
+
inverse_fold: ${model.inverse_fold}
|
| 95 |
+
use_msa: false
|
| 96 |
+
selector:
|
| 97 |
+
_target_: boltzgen.data.select.protein.ProteinSelector
|
| 98 |
+
design_neighborhood_sizes: [2, 4, 6,8,10,12,14,16,18]
|
| 99 |
+
substructure_neighborhood_sizes: [2,4,6,8,10,12,24]
|
| 100 |
+
structure_condition_prob: 0.5
|
| 101 |
+
distance_noise_std: 1
|
| 102 |
+
run_selection: true
|
| 103 |
+
specify_binding_sites: false
|
| 104 |
+
ss_condition_prob: 0
|
| 105 |
+
select_all: true
|
| 106 |
+
complete_structure_mask: true
|
| 107 |
+
|
| 108 |
+
# Design datasets
|
| 109 |
+
monomer_split: ./model/boltzgen/resources/splits/val_monomers_boltzgen_min50_max220.txt
|
| 110 |
+
monomer_target_dir: ./training_data/targets
|
| 111 |
+
monomer_target_structure_condition: true
|
| 112 |
+
monomer_seq_len: 100
|
| 113 |
+
|
| 114 |
+
ligand_split: null
|
| 115 |
+
ligand_target_dir: ./training_data/targets
|
| 116 |
+
ligand_seq_len: 100
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
model:
|
| 120 |
+
_target_: boltzgen.model.models.boltz.Boltz
|
| 121 |
+
atom_s: 128
|
| 122 |
+
atom_z: 16
|
| 123 |
+
token_s: 384
|
| 124 |
+
token_z: ${model.inverse_fold_args.pair_dim}
|
| 125 |
+
num_bins: 64
|
| 126 |
+
atom_feature_dim: 388
|
| 127 |
+
atoms_per_window_queries: 32
|
| 128 |
+
atoms_per_window_keys: 128
|
| 129 |
+
use_miniformer: true
|
| 130 |
+
ema: true
|
| 131 |
+
ema_decay: 0.999
|
| 132 |
+
exclude_ions_from_lddt: true
|
| 133 |
+
num_val_datasets: 1 # New
|
| 134 |
+
ignore_ckpt_shape_mismatch: false # New
|
| 135 |
+
aggregate_distogram: true # New
|
| 136 |
+
bond_type_feature: true
|
| 137 |
+
predict_bfactor: true
|
| 138 |
+
predict_res_type: true
|
| 139 |
+
checkpoint_diffusion_conditioning: false
|
| 140 |
+
inverse_fold: true
|
| 141 |
+
inverse_fold_args:
|
| 142 |
+
atom_s: ${model.atom_s}
|
| 143 |
+
atom_z: ${model.atom_z}
|
| 144 |
+
token_s: ${model.token_s}
|
| 145 |
+
token_z: ${model.token_z}
|
| 146 |
+
node_dim: 128
|
| 147 |
+
pair_dim: 128
|
| 148 |
+
hidden_dim: 128
|
| 149 |
+
dropout: 0.1
|
| 150 |
+
softmax_dropout: 0.2
|
| 151 |
+
num_encoder_layers: 6
|
| 152 |
+
num_decoder_layers: 3
|
| 153 |
+
autoregressive: true
|
| 154 |
+
transformation_scale_factor: 1.0
|
| 155 |
+
inverse_fold_noise: 0.2
|
| 156 |
+
topk: 30
|
| 157 |
+
num_heads: 4
|
| 158 |
+
enable_input_embedder: True
|
| 159 |
+
sampling_temperature: -1.0
|
| 160 |
+
|
| 161 |
+
validators:
|
| 162 |
+
- _target_: boltzgen.model.validation.design.DesignValidator
|
| 163 |
+
val_names: ["RCSB"]
|
| 164 |
+
confidence_prediction: ${model.confidence_prediction}
|
| 165 |
+
atom14: ${data.atom14}
|
| 166 |
+
atom37: ${data.atom37}
|
| 167 |
+
backbone_only: ${data.backbone_only}
|
| 168 |
+
inverse_fold: ${model.inverse_fold}
|
| 169 |
+
|
| 170 |
+
masker_args:
|
| 171 |
+
mask: true
|
| 172 |
+
mask_backbone: false
|
| 173 |
+
mask_disto: false
|
| 174 |
+
|
| 175 |
+
embedder_args:
|
| 176 |
+
atom_encoder_depth: 1
|
| 177 |
+
atom_encoder_heads: 4
|
| 178 |
+
add_mol_type_feat: true
|
| 179 |
+
add_method_conditioning: true
|
| 180 |
+
add_modified_flag: true
|
| 181 |
+
add_cyclic_flag: true
|
| 182 |
+
add_design_mask_flag: false
|
| 183 |
+
add_binding_specification: false
|
| 184 |
+
add_ss_specification: false
|
| 185 |
+
|
| 186 |
+
use_token_distances: false
|
| 187 |
+
token_distance_args:
|
| 188 |
+
token_distance_dim: ${model.inverse_fold_args.pair_dim}
|
| 189 |
+
token_distance_blocks: 0
|
| 190 |
+
use_token_distance_feats: true
|
| 191 |
+
distance_gaussian_dim: 32
|
| 192 |
+
disable_token_distance_transition: true
|
| 193 |
+
use_relative_position_encoding: true
|
| 194 |
+
|
| 195 |
+
# MSA module is not used in inverse folding
|
| 196 |
+
msa_args:
|
| 197 |
+
msa_s: 2
|
| 198 |
+
msa_blocks: 0
|
| 199 |
+
msa_dropout: 0
|
| 200 |
+
z_dropout: 0
|
| 201 |
+
miniformer_blocks: true
|
| 202 |
+
pairwise_head_width: 2
|
| 203 |
+
pairwise_num_heads: 1
|
| 204 |
+
use_paired_feature: true
|
| 205 |
+
activation_checkpointing: false
|
| 206 |
+
|
| 207 |
+
pairformer_args:
|
| 208 |
+
num_blocks: 2
|
| 209 |
+
num_heads: 16
|
| 210 |
+
dropout: 0.25
|
| 211 |
+
post_layer_norm: false
|
| 212 |
+
activation_checkpointing: false
|
| 213 |
+
|
| 214 |
+
score_model_args:
|
| 215 |
+
sigma_data: 16
|
| 216 |
+
dim_fourier: 256
|
| 217 |
+
atom_encoder_depth: 3
|
| 218 |
+
atom_encoder_heads: 4
|
| 219 |
+
|
| 220 |
+
# token level args
|
| 221 |
+
token_layers: 1
|
| 222 |
+
token_transformer_depth: 3
|
| 223 |
+
token_transformer_heads: 16
|
| 224 |
+
diffusion_pairformer_args:
|
| 225 |
+
num_blocks: 0
|
| 226 |
+
num_heads: 2
|
| 227 |
+
dropout: 0
|
| 228 |
+
use_s_to_z: false
|
| 229 |
+
|
| 230 |
+
atom_decoder_depth: 3
|
| 231 |
+
atom_decoder_heads: 4
|
| 232 |
+
conditioning_transition_layers: 2
|
| 233 |
+
transformer_post_ln: false
|
| 234 |
+
activation_checkpointing: false
|
| 235 |
+
|
| 236 |
+
confidence_prediction: false
|
| 237 |
+
affinity_prediction: false
|
| 238 |
+
structure_prediction_training: true
|
| 239 |
+
affinity_model_args:
|
| 240 |
+
num_dist_bins: 64
|
| 241 |
+
max_dist: 22
|
| 242 |
+
no_trunk_feats: false
|
| 243 |
+
add_s_to_z_prod: false
|
| 244 |
+
add_s_input_to_s: false
|
| 245 |
+
|
| 246 |
+
confidence_args:
|
| 247 |
+
num_plddt_bins: 50
|
| 248 |
+
num_pde_bins: 64
|
| 249 |
+
num_pae_bins: 64
|
| 250 |
+
|
| 251 |
+
training_args:
|
| 252 |
+
recycling_steps: 0
|
| 253 |
+
sampling_steps: 20
|
| 254 |
+
diffusion_multiplicity: 2
|
| 255 |
+
diffusion_samples: 1
|
| 256 |
+
affinity_loss_weight: 3e-3
|
| 257 |
+
confidence_loss_weight: 1e-4
|
| 258 |
+
diffusion_loss_weight: 4.0
|
| 259 |
+
distogram_loss_weight: 3e-2
|
| 260 |
+
bfactor_loss_weight: 1e-3
|
| 261 |
+
res_type_loss_weight: 1
|
| 262 |
+
adam_beta_1: 0.9
|
| 263 |
+
adam_beta_2: 0.95
|
| 264 |
+
adam_eps: 0.00000001
|
| 265 |
+
lr_scheduler: onecycle
|
| 266 |
+
base_lr: 0.0
|
| 267 |
+
max_lr: 0.001
|
| 268 |
+
weight_decay: 0.003
|
| 269 |
+
weight_decay_exclude: true
|
| 270 |
+
|
| 271 |
+
validation_args:
|
| 272 |
+
recycling_steps: 0
|
| 273 |
+
sampling_steps: 200
|
| 274 |
+
diffusion_samples: 1
|
| 275 |
+
symmetry_correction: false
|
| 276 |
+
|
| 277 |
+
diffusion_process_args:
|
| 278 |
+
sigma_min: 0.0004 # min noise level
|
| 279 |
+
sigma_max: 160.0 # max noise level
|
| 280 |
+
sigma_data: 16.0 # standard deviation of data distribution
|
| 281 |
+
rho: 7 # controls the sampling schedule
|
| 282 |
+
P_mean: -1.2 # mean of log-normal distribution from which noise is drawn for training
|
| 283 |
+
P_std: 1.5 # standard deviation of log-normal distribution from which noise is drawn for training
|
| 284 |
+
gamma_0: 0.8
|
| 285 |
+
gamma_min: 1.0
|
| 286 |
+
noise_scale: 1.0
|
| 287 |
+
step_scale: 1.0
|
| 288 |
+
mse_rotational_alignment: true
|
| 289 |
+
coordinate_augmentation: true
|
| 290 |
+
alignment_reverse_diff: true
|
| 291 |
+
synchronize_sigmas: false
|
| 292 |
+
|
| 293 |
+
diffusion_loss_args:
|
| 294 |
+
add_smooth_lddt_loss: true
|
| 295 |
+
add_bond_loss: false
|
| 296 |
+
nucleotide_loss_weight: 5.0
|
| 297 |
+
ligand_loss_weight: 10.0
|
| 298 |
+
|
| 299 |
+
refolding_validator:
|
| 300 |
+
_target_: boltzgen.model.validation.refolding.RefoldingValidator
|
| 301 |
+
val_names: ["RCSB"]
|
| 302 |
+
step_scale: 1.5
|
| 303 |
+
noise_scale: 0.75
|
| 304 |
+
atom14: ${data.atom14}
|
| 305 |
+
atom37: ${data.atom37}
|
| 306 |
+
val_monomer: ${data.monomer_split}
|
| 307 |
+
val_ligand: ${data.ligand_split}
|
| 308 |
+
inverse_fold: ${model.inverse_fold}
|
| 309 |
+
analyze_task:
|
| 310 |
+
_target_: boltzgen.task.analyze.analyze.Analyze
|
| 311 |
+
name: ${name}
|
| 312 |
+
debug: ${debug}
|
| 313 |
+
design_dir: null
|
| 314 |
+
num_processes: 1
|
| 315 |
+
|
| 316 |
+
# Common metrics to compute
|
| 317 |
+
affinity_metrics: false
|
| 318 |
+
allatom_fold_metrics: true
|
| 319 |
+
backbone_fold_metrics: true
|
| 320 |
+
noncovalents_original: false
|
| 321 |
+
noncovalents_refolded: false
|
| 322 |
+
delta_sasa_original: false
|
| 323 |
+
delta_sasa_refolded: false
|
| 324 |
+
largest_hydrophobic: false
|
| 325 |
+
largest_hydrophobic_refolded: false
|
| 326 |
+
run_clustering: false
|
| 327 |
+
|
| 328 |
+
# Liability analysis
|
| 329 |
+
liability_analysis: false
|
| 330 |
+
liability_modality: peptide
|
| 331 |
+
liability_peptide_type: linear
|
| 332 |
+
|
| 333 |
+
# Uncommon metrics
|
| 334 |
+
diversity_original: true
|
| 335 |
+
diversity_refolded: true
|
| 336 |
+
diversity_per_target_original: false
|
| 337 |
+
diversity_per_target_refolded: false
|
| 338 |
+
novelty_original: false
|
| 339 |
+
novelty_refolded: false
|
| 340 |
+
novelty_per_target_original: false
|
| 341 |
+
novelty_per_target_refolded: false
|
| 342 |
+
|
| 343 |
+
wandb: null
|
| 344 |
+
|
| 345 |
+
data:
|
| 346 |
+
_target_: boltzgen.task.predict.data_from_generated.FromGeneratedDataModule
|
| 347 |
+
cfg:
|
| 348 |
+
_target_: boltzgen.task.predict.data_from_generated.DataConfig
|
| 349 |
+
tokenizer:
|
| 350 |
+
_target_: boltzgen.data.tokenize.tokenizer.Tokenizer
|
| 351 |
+
atomize_modified_residues: false
|
| 352 |
+
featurizer:
|
| 353 |
+
_target_: boltzgen.data.feature.featurizer.Featurizer
|
| 354 |
+
|
| 355 |
+
suffix: .cif
|
| 356 |
+
suffix_metadata: .npz
|
| 357 |
+
suffix_native: _native.cif
|
| 358 |
+
samples_per_target: 1
|
| 359 |
+
num_targets: 100000000
|
| 360 |
+
moldir: ./training_data/mols
|
| 361 |
+
|
| 362 |
+
batch_size: 1
|
| 363 |
+
num_workers: 1
|
| 364 |
+
pin_memory: false
|
| 365 |
+
target_templates: true
|
| 366 |
+
return_native: true
|
| 367 |
+
|
| 368 |
+
folding_checkpoint: ./training_data/boltz2_fold.ckpt
|
| 369 |
+
|
| 370 |
+
folding_args:
|
| 371 |
+
recycling_steps: 3
|
| 372 |
+
sampling_steps: 200
|
| 373 |
+
diffusion_samples: 1
|
| 374 |
+
|
| 375 |
+
folding_model_args:
|
| 376 |
+
validators: null
|
model/boltzgen/resources/main.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from typing import List
|
| 3 |
+
import hydra
|
| 4 |
+
import omegaconf
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
from boltzgen.task.task import Task
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main(config: str, args: List) -> None:
|
| 11 |
+
"""
|
| 12 |
+
This is just a wrapper for running the .run() function of our `Task` class.
|
| 13 |
+
If you run the pipeline (for example via `boltzgen run design_spec.yaml ...`) then this function reads the yaml files of the individual pipeline steps and executes the pipeline steps.
|
| 14 |
+
|
| 15 |
+
The possible tasks are:
|
| 16 |
+
- Train (GPU: BoltzGen diffusion model or inverse folding model training)
|
| 17 |
+
- Predict (GPU: Running BoltzGen diffusion, inverse folding, refolding, designfolding, or affinity prediction)
|
| 18 |
+
- Analyze (CPU: Compute CPU Metrics and aggregate metrics from GPU steps)
|
| 19 |
+
- Filter (CPU: Very fast (20s) computes ranking and writes final output files)
|
| 20 |
+
|
| 21 |
+
The files for these are:
|
| 22 |
+
- src/boltzgen/task/train/train.py
|
| 23 |
+
- src/boltzgen/task/predict/predict.py
|
| 24 |
+
- src/boltzgen/task/analyze/analyze.py
|
| 25 |
+
- src/boltzgen/task/filter/filter.py
|
| 26 |
+
|
| 27 |
+
Parameters
|
| 28 |
+
----------
|
| 29 |
+
config : str
|
| 30 |
+
Path to the configuration yaml file. The yaml file contains something like `_target_: boltzgen.task.predict.predict.Predict` at the beginning which tells it which Task class to run
|
| 31 |
+
args : List
|
| 32 |
+
List of arguments to override the configuration.
|
| 33 |
+
"""
|
| 34 |
+
# Load the configuration
|
| 35 |
+
args = omegaconf.OmegaConf.from_dotlist(args)
|
| 36 |
+
config = omegaconf.OmegaConf.load(config)
|
| 37 |
+
config = omegaconf.OmegaConf.merge(config, args)
|
| 38 |
+
|
| 39 |
+
# Instantiate the task
|
| 40 |
+
task = hydra.utils.instantiate(config)
|
| 41 |
+
|
| 42 |
+
if not isinstance(task, Task):
|
| 43 |
+
msg = "Config must be an instance of Task."
|
| 44 |
+
raise TypeError(msg)
|
| 45 |
+
|
| 46 |
+
# Run the task
|
| 47 |
+
task.run(config)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
config = sys.argv[1]
|
| 52 |
+
args = sys.argv[2:]
|
| 53 |
+
main(config, args)
|
model/boltzgen/resources/metrics_normalization.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"design_iiptm": {
|
| 3 |
+
"mean": 0.3828427453913554,
|
| 4 |
+
"std": 0.10621994686776445
|
| 5 |
+
},
|
| 6 |
+
"design_ptm": {
|
| 7 |
+
"mean": 0.6548983461494845,
|
| 8 |
+
"std": 0.041925380367595154
|
| 9 |
+
},
|
| 10 |
+
"min_design_to_target_pae": {
|
| 11 |
+
"mean": 8.665374909044512,
|
| 12 |
+
"std": 3.8643855151416453
|
| 13 |
+
},
|
| 14 |
+
"design_hydrophobicity": {
|
| 15 |
+
"mean": 43.95844996189343,
|
| 16 |
+
"std": 7.565721364606706
|
| 17 |
+
},
|
| 18 |
+
"design_largest_hydrophobic_patch_refolded": {
|
| 19 |
+
"mean": 608.7238163271427,
|
| 20 |
+
"std": 440.3199461402019
|
| 21 |
+
},
|
| 22 |
+
"delta_sasa_refolded": {
|
| 23 |
+
"mean": 1368.936707104108,
|
| 24 |
+
"std": 514.8573173243741
|
| 25 |
+
},
|
| 26 |
+
"plip_saltbridge_refolded": {
|
| 27 |
+
"mean": 1.746922289263117,
|
| 28 |
+
"std": 1.9177749177139145
|
| 29 |
+
},
|
| 30 |
+
"plip_hbonds_refolded": {
|
| 31 |
+
"mean": 7.099706654392513,
|
| 32 |
+
"std": 4.456755351208993
|
| 33 |
+
},
|
| 34 |
+
"affinity_probability_binary1": {
|
| 35 |
+
"mean": 0.4726671722992441,
|
| 36 |
+
"std": 0.19092615494037402
|
| 37 |
+
}
|
| 38 |
+
}
|
model/boltzgen/resources/splits/val_ccd_pdb_pairs_boltzgen.txt
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
WF6_7FZC
|
| 2 |
+
WHI_7FZP
|
| 3 |
+
LM9_7FWB
|
| 4 |
+
WON_7G1L
|
| 5 |
+
ZI8_8SHJ
|
| 6 |
+
SWO_8J19
|
| 7 |
+
SVI_7GO2
|
| 8 |
+
IQH_7FVW
|
| 9 |
+
QKO_8BOE
|
| 10 |
+
WQC_7G1Y
|
| 11 |
+
TQO_7FXR
|
| 12 |
+
IUX_7ZG3
|
| 13 |
+
VMR_8OG7
|
| 14 |
+
LKX_7GCF
|
| 15 |
+
W9K_7FYS
|
| 16 |
+
LS9_8ADR
|
| 17 |
+
5IY_8HXO
|
| 18 |
+
ZV8_7G9Y
|
| 19 |
+
JGX_7YMJ
|
| 20 |
+
VI0_7FY6
|
| 21 |
+
WPZ_7G1T
|
| 22 |
+
TE0_7GP7
|
| 23 |
+
WQH_7G1Z
|
| 24 |
+
QLL_7FX7
|
| 25 |
+
WEN_7FZB
|
| 26 |
+
Q0O_7FWU
|
| 27 |
+
OT9_7FWP
|
| 28 |
+
ZDF_8GEM
|
| 29 |
+
5YT_8HXP
|
| 30 |
+
UTO_8CJ7
|
| 31 |
+
S8X_7FXK
|
| 32 |
+
Q63_5SO3
|
| 33 |
+
U0O_7GQN
|
| 34 |
+
Z4I_8GD4
|
| 35 |
+
YXW_8G89
|
| 36 |
+
ZCF_8GEU
|
| 37 |
+
60J_8HYD
|
| 38 |
+
5Z2_8HXU
|
| 39 |
+
NOI_7GEK
|
| 40 |
+
IWI_7FVY
|
| 41 |
+
XE3_8WSM
|
| 42 |
+
5ZX_8JAO
|
| 43 |
+
ZQ2_8PH7
|
| 44 |
+
WYW_8H7N
|
| 45 |
+
WYO_7G15
|
| 46 |
+
TJA_7GPN
|
| 47 |
+
P6X_7FWR
|
| 48 |
+
ZUG_7G9Q
|
| 49 |
+
X7C_8P8K
|
| 50 |
+
5ZU_8HY1
|
| 51 |
+
P9X_8IHK
|
| 52 |
+
SE1_8DJ3
|
| 53 |
+
TZ9_7GO5
|
| 54 |
+
TQC_7GOJ
|
| 55 |
+
QXF_8BJK
|
| 56 |
+
VJ3_7FYC
|
| 57 |
+
OUF_7GGD
|
| 58 |
+
W7U_7FYI
|
| 59 |
+
SY0_8C11
|
| 60 |
+
O6Q_7FWL
|
| 61 |
+
O9U_7FWM
|
| 62 |
+
M1U_8AGP
|
| 63 |
+
R80_7FXC
|
| 64 |
+
IKQ_7G21
|
| 65 |
+
XNO_8FX5
|
| 66 |
+
ZDK_8GEV
|
| 67 |
+
WMQ_7G17
|
| 68 |
+
NGX_7GED
|
| 69 |
+
OD3_8I67
|
| 70 |
+
LO0_7GCO
|
| 71 |
+
KOX_8A0S
|
| 72 |
+
WGW_7FZM
|
| 73 |
+
YJN_8G3C
|
| 74 |
+
WKN_7G0L
|
| 75 |
+
Z5H_8GD2
|
| 76 |
+
WHN_7FZR
|
| 77 |
+
TJL_7GPE
|
| 78 |
+
UOF_7G0K
|
| 79 |
+
NQ3_7GEL
|
| 80 |
+
NRX_7GEO
|
| 81 |
+
WEA_7FZ6
|
| 82 |
+
UOR_7FY2
|
| 83 |
+
5ZD_8HXV
|
| 84 |
+
ST0_7GNX
|
| 85 |
+
VVL_7FYG
|
| 86 |
+
U7F_8CC4
|
| 87 |
+
NT6_7UMO
|
| 88 |
+
MWR_7FWG
|
| 89 |
+
W9W_7FYX
|
| 90 |
+
WMF_7G10
|
| 91 |
+
ITF_8HOC
|
| 92 |
+
TZJ_7FXT
|
| 93 |
+
Y2S_8E1O
|
| 94 |
+
W8C_7FYK
|
| 95 |
+
QG0_7FX0
|
| 96 |
+
YW6_8PIF
|
| 97 |
+
WYN_8H7I
|
| 98 |
+
QBX_7FWY
|
| 99 |
+
U2I_8DII
|
| 100 |
+
R9O_7FXF
|
| 101 |
+
Q7E_5SOA
|
model/boltzgen/resources/splits/val_monomers_boltzgen_min50_max220.txt
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
8GM6
|
| 2 |
+
8P35
|
| 3 |
+
7Z3G
|
| 4 |
+
8T9N
|
| 5 |
+
8F8N
|
| 6 |
+
8B6E
|
| 7 |
+
8TGB
|
| 8 |
+
8J9Q
|
| 9 |
+
8P5N
|
| 10 |
+
8BSA
|
| 11 |
+
7Z3F
|
| 12 |
+
8B4C
|
| 13 |
+
8H0L
|
| 14 |
+
8GM7
|
| 15 |
+
8CGM
|
| 16 |
+
8BAU
|
| 17 |
+
7YSF
|
| 18 |
+
8J0H
|
| 19 |
+
8OXH
|
| 20 |
+
8R8C
|
| 21 |
+
8CM1
|
| 22 |
+
8BL3
|
| 23 |
+
8OQH
|
| 24 |
+
8EY3
|
| 25 |
+
8JNG
|
| 26 |
+
8JN0
|
| 27 |
+
8EV5
|
| 28 |
+
7YD4
|
| 29 |
+
8B2E
|
| 30 |
+
8BAS
|
| 31 |
+
8BBZ
|
| 32 |
+
8BAQ
|
| 33 |
+
8JNM
|
| 34 |
+
8OYW
|
| 35 |
+
8SYJ
|
| 36 |
+
7Y5M
|
| 37 |
+
7QRL
|
| 38 |
+
7Y19
|
| 39 |
+
7Z3B
|
| 40 |
+
8GSY
|
| 41 |
+
8AN4
|
| 42 |
+
8UK7
|
| 43 |
+
8GN3
|
| 44 |
+
8V1K
|
| 45 |
+
8OXL
|
| 46 |
+
8T8K
|
| 47 |
+
8OYV
|
| 48 |
+
8T8L
|
| 49 |
+
8B2G
|
| 50 |
+
7WLH
|
| 51 |
+
8HNA
|
| 52 |
+
8TFS
|
| 53 |
+
8FJG
|
| 54 |
+
8JU8
|
| 55 |
+
8OK3
|
| 56 |
+
8P2A
|
| 57 |
+
7WU8
|
| 58 |
+
7XP9
|
| 59 |
+
8J98
|
| 60 |
+
8T4C
|
| 61 |
+
8EOV
|
| 62 |
+
8GJ9
|
| 63 |
+
8OVU
|
| 64 |
+
7TAL
|
| 65 |
+
7VS2
|
| 66 |
+
8OXJ
|
| 67 |
+
8AH9
|
| 68 |
+
8BL6
|
| 69 |
+
8P5P
|
| 70 |
+
8P4Y
|
| 71 |
+
8BSB
|
| 72 |
+
8EB9
|
| 73 |
+
8SXS
|
| 74 |
+
8GYR
|
| 75 |
+
8P2B
|
| 76 |
+
7YKM
|
| 77 |
+
8OYS
|
| 78 |
+
8JNL
|
| 79 |
+
8T5J
|
| 80 |
+
8VEH
|
| 81 |
+
8OXK
|
| 82 |
+
8TNM
|
| 83 |
+
8BGB
|
| 84 |
+
8EV4
|
| 85 |
+
8BAR
|
| 86 |
+
8EHN
|
| 87 |
+
8J1X
|
| 88 |
+
8FJF
|
| 89 |
+
8HJJ
|
| 90 |
+
8KCA
|
| 91 |
+
8GN4
|
| 92 |
+
7Z3I
|
| 93 |
+
8F5G
|
| 94 |
+
8G64
|
| 95 |
+
8R8A
|
| 96 |
+
8SOT
|
| 97 |
+
8G8K
|
| 98 |
+
8INI
|
| 99 |
+
8J1W
|
| 100 |
+
8JWS
|
| 101 |
+
8U0X
|
| 102 |
+
8GMX
|
| 103 |
+
8J9R
|
| 104 |
+
8AXJ
|
| 105 |
+
8EC3
|
| 106 |
+
8JWD
|
| 107 |
+
8FR5
|
| 108 |
+
8GTH
|
| 109 |
+
8BHU
|
| 110 |
+
8FZY
|
model/boltzgen/resources/splits/validation_ids_boltz2_all.txt
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
8Q41
|
| 2 |
+
8BH8
|
| 3 |
+
8BH9
|
| 4 |
+
8HIG
|
| 5 |
+
8IA3
|
| 6 |
+
8SVD
|
| 7 |
+
7ZLQ
|
| 8 |
+
8TPK
|
| 9 |
+
8Q3Z
|
| 10 |
+
8Q40
|
| 11 |
+
8K3D
|
| 12 |
+
8SVA
|
| 13 |
+
8D9I
|
| 14 |
+
8SSU
|
| 15 |
+
8PE3
|
| 16 |
+
8GN3
|
| 17 |
+
8GN4
|
| 18 |
+
8K4L
|
| 19 |
+
8EB5
|
| 20 |
+
8B4E
|
| 21 |
+
8Q43
|
| 22 |
+
8H0L
|
| 23 |
+
8TP8
|
| 24 |
+
8SSQ
|
| 25 |
+
8F5G
|
| 26 |
+
8SQU
|
| 27 |
+
8DWJ
|
| 28 |
+
8Q42
|
| 29 |
+
8D9E
|
| 30 |
+
8Q44
|
| 31 |
+
8ISZ
|
| 32 |
+
7XF1
|
| 33 |
+
8HML
|
| 34 |
+
8SSR
|
| 35 |
+
8B4D
|
| 36 |
+
8B4C
|
| 37 |
+
7YSF
|
| 38 |
+
8J0K
|
| 39 |
+
8J0R
|
| 40 |
+
8GBA
|
| 41 |
+
8GTY
|
| 42 |
+
8GBM
|
| 43 |
+
7YL4
|
| 44 |
+
8ANU
|
| 45 |
+
8K3F
|
| 46 |
+
8J7J
|
| 47 |
+
8CQM
|
| 48 |
+
8CLZ
|
| 49 |
+
8HI7
|
| 50 |
+
7YH3
|
| 51 |
+
8OGG
|
| 52 |
+
8CR3
|
| 53 |
+
8GBH
|
| 54 |
+
8BCK
|
| 55 |
+
7YPR
|
| 56 |
+
8ILL
|
| 57 |
+
8OK7
|
| 58 |
+
8FR5
|
| 59 |
+
8BU0
|
| 60 |
+
8OYY
|
| 61 |
+
7XVI
|
| 62 |
+
8JWS
|
| 63 |
+
8R8A
|
| 64 |
+
8Q1K
|
| 65 |
+
8IN6
|
| 66 |
+
8JI2
|
| 67 |
+
8GD6
|
| 68 |
+
8JDG
|
| 69 |
+
8HHV
|
| 70 |
+
8JT9
|
| 71 |
+
8QFN
|
| 72 |
+
8GKX
|
| 73 |
+
8ACG
|
| 74 |
+
8AVZ
|
| 75 |
+
8BHU
|
| 76 |
+
8GL4
|
| 77 |
+
8CHX
|
| 78 |
+
7Z3I
|
| 79 |
+
8UPI
|
| 80 |
+
8DQ2
|
| 81 |
+
8HVC
|
| 82 |
+
8OQH
|
| 83 |
+
7Y9G
|
| 84 |
+
7YH5
|
| 85 |
+
8B2H
|
| 86 |
+
8IQC
|
| 87 |
+
7YN2
|
| 88 |
+
8I3X
|
| 89 |
+
7YR9
|
| 90 |
+
8CH4
|
| 91 |
+
8GOJ
|
| 92 |
+
8FID
|
| 93 |
+
7T4W
|
| 94 |
+
8D40
|
| 95 |
+
8HFP
|
| 96 |
+
8K5K
|
| 97 |
+
8GBI
|
| 98 |
+
8OW8
|
| 99 |
+
8Q70
|
| 100 |
+
8SDY
|
| 101 |
+
8JMR
|
| 102 |
+
8IIB
|
| 103 |
+
8SW0
|
| 104 |
+
8DQ6
|
| 105 |
+
8B6Q
|
| 106 |
+
8H2N
|
| 107 |
+
8SUT
|
| 108 |
+
8J9Q
|
| 109 |
+
8IW5
|
| 110 |
+
8BL6
|
| 111 |
+
8BBR
|
| 112 |
+
8JWU
|
| 113 |
+
8XBI
|
| 114 |
+
8B2G
|
| 115 |
+
8ASA
|
| 116 |
+
8B2E
|
| 117 |
+
8EY3
|
| 118 |
+
8GJ9
|
| 119 |
+
7ZAO
|
| 120 |
+
8B2S
|
| 121 |
+
8GD8
|
| 122 |
+
8A9N
|
| 123 |
+
8TN8
|
| 124 |
+
8I34
|
| 125 |
+
7Y1S
|
| 126 |
+
8F9X
|
| 127 |
+
8PXC
|
| 128 |
+
8B5W
|
| 129 |
+
8EC3
|
| 130 |
+
8HFC
|
| 131 |
+
8GYG
|
| 132 |
+
8IR2
|
| 133 |
+
8CJG
|
| 134 |
+
8BUX
|
| 135 |
+
7UWU
|
| 136 |
+
8GYR
|
| 137 |
+
8EHN
|
| 138 |
+
7ZCB
|
| 139 |
+
7YH1
|
| 140 |
+
8IUB
|
| 141 |
+
8BVK
|
| 142 |
+
8OYE
|
| 143 |
+
8GJ7
|
| 144 |
+
8C4D
|
| 145 |
+
8FTV
|
| 146 |
+
8GIV
|
| 147 |
+
8T8K
|
| 148 |
+
8JZU
|
| 149 |
+
8A26
|
| 150 |
+
8HC0
|
| 151 |
+
8EO2
|
| 152 |
+
8H72
|
| 153 |
+
8HMM
|
| 154 |
+
8ORU
|
| 155 |
+
8C05
|
| 156 |
+
8G95
|
| 157 |
+
8BSA
|
| 158 |
+
7ZC9
|
| 159 |
+
8IF7
|
| 160 |
+
8QC6
|
| 161 |
+
7QP5
|
| 162 |
+
7YLS
|
| 163 |
+
8TN3
|
| 164 |
+
8ANI
|
| 165 |
+
7ZET
|
| 166 |
+
8AVH
|
| 167 |
+
8D2Z
|
| 168 |
+
8THR
|
| 169 |
+
8K3B
|
| 170 |
+
8J1C
|
| 171 |
+
7Y4R
|
| 172 |
+
8U2O
|
| 173 |
+
8BTJ
|
| 174 |
+
8HND
|
| 175 |
+
8IN1
|
| 176 |
+
8JX6
|
| 177 |
+
7YLR
|
| 178 |
+
8GK6
|
| 179 |
+
8B7D
|
| 180 |
+
8BGJ
|
| 181 |
+
8P0S
|
| 182 |
+
8B4L
|
| 183 |
+
8JJ5
|
| 184 |
+
8EVX
|
| 185 |
+
8H24
|
| 186 |
+
8OF7
|
| 187 |
+
8IK2
|
| 188 |
+
8P5N
|
| 189 |
+
8GJA
|
| 190 |
+
7YTL
|
| 191 |
+
8H4P
|
| 192 |
+
8KI5
|
| 193 |
+
8T5J
|
| 194 |
+
8GHH
|
| 195 |
+
7ZN6
|
| 196 |
+
8JNG
|
| 197 |
+
8GR7
|
| 198 |
+
8H7J
|
| 199 |
+
8CI3
|
| 200 |
+
8AH9
|
| 201 |
+
8BS3
|
| 202 |
+
8AGA
|
| 203 |
+
8U01
|
| 204 |
+
8HN2
|
| 205 |
+
8H5A
|
| 206 |
+
7YRO
|
| 207 |
+
8AN5
|
| 208 |
+
8HLG
|
| 209 |
+
8OKS
|
| 210 |
+
8K4X
|
| 211 |
+
8D2J
|
| 212 |
+
8PKD
|
| 213 |
+
7VXT
|
| 214 |
+
8H3Z
|
| 215 |
+
8TJI
|
| 216 |
+
8K76
|
| 217 |
+
8CIL
|
| 218 |
+
8HF2
|
| 219 |
+
8P6K
|
| 220 |
+
7YW0
|
| 221 |
+
8C26
|
| 222 |
+
8BV8
|
| 223 |
+
8BS0
|
| 224 |
+
8SME
|
| 225 |
+
8I8A
|
| 226 |
+
7YJI
|
| 227 |
+
8EOZ
|
| 228 |
+
8OML
|
| 229 |
+
8BUY
|
| 230 |
+
8OWI
|
| 231 |
+
8DTQ
|
| 232 |
+
8P26
|
| 233 |
+
8EZT
|
| 234 |
+
8FW7
|
| 235 |
+
7Z8Z
|
| 236 |
+
8DQA
|
| 237 |
+
8K5L
|
| 238 |
+
8PR6
|
| 239 |
+
8K1F
|
| 240 |
+
8AXR
|
| 241 |
+
8JVM
|
| 242 |
+
8ANT
|
| 243 |
+
8ER5
|
| 244 |
+
8OXU
|
| 245 |
+
8H2R
|
| 246 |
+
7YPE
|
| 247 |
+
8WOJ
|
| 248 |
+
8SS1
|
| 249 |
+
8G5S
|
| 250 |
+
8G2V
|
| 251 |
+
8SAO
|
| 252 |
+
8A50
|
| 253 |
+
8BDK
|
| 254 |
+
7YIL
|
| 255 |
+
8ARD
|
| 256 |
+
8KGZ
|
| 257 |
+
8HO2
|
| 258 |
+
8ECX
|
| 259 |
+
8P32
|
| 260 |
+
8AGY
|
| 261 |
+
8SZB
|
| 262 |
+
8ONB
|
| 263 |
+
8DSP
|
| 264 |
+
8BJW
|
| 265 |
+
8U1J
|
| 266 |
+
8FIH
|
| 267 |
+
8KE8
|
| 268 |
+
8SJJ
|
| 269 |
+
8E2B
|
| 270 |
+
8U1K
|
| 271 |
+
8POC
|
| 272 |
+
8HNO
|
| 273 |
+
8K4W
|
| 274 |
+
8UC6
|
| 275 |
+
8AMR
|
| 276 |
+
8ON4
|
| 277 |
+
8PHB
|
| 278 |
+
8IW0
|
| 279 |
+
8OKW
|
| 280 |
+
7Y16
|
| 281 |
+
8PX4
|
| 282 |
+
8BRF
|
| 283 |
+
8BZ2
|
| 284 |
+
8HGU
|
| 285 |
+
8ERW
|
| 286 |
+
8GAQ
|
| 287 |
+
8HM5
|
| 288 |
+
7YKH
|
| 289 |
+
8SIU
|
| 290 |
+
8G0K
|
| 291 |
+
8ONQ
|
| 292 |
+
7XPC
|
| 293 |
+
8BIY
|
| 294 |
+
8BNB
|
| 295 |
+
8U12
|
| 296 |
+
8FJE
|
| 297 |
+
8A39
|
| 298 |
+
8FIT
|
| 299 |
+
8SRZ
|
| 300 |
+
8HUC
|
| 301 |
+
7W91
|
| 302 |
+
7XRB
|
| 303 |
+
8R79
|
| 304 |
+
8AU0
|
| 305 |
+
8OFJ
|
| 306 |
+
8H1K
|
| 307 |
+
8BCS
|
| 308 |
+
8TTO
|
| 309 |
+
8B3Z
|
| 310 |
+
8EBB
|
| 311 |
+
8QK8
|
| 312 |
+
8OIJ
|
| 313 |
+
8B45
|
| 314 |
+
8EOX
|
| 315 |
+
8EK4
|
| 316 |
+
8E1E
|
| 317 |
+
8ONF
|
| 318 |
+
8HHJ
|
| 319 |
+
8ENQ
|
| 320 |
+
7YEQ
|
| 321 |
+
8TNO
|
| 322 |
+
8AN0
|
| 323 |
+
8OK3
|
| 324 |
+
8T4C
|
| 325 |
+
8G8K
|
| 326 |
+
8I3J
|
| 327 |
+
8HDL
|
| 328 |
+
8AXJ
|
| 329 |
+
8GY4
|
| 330 |
+
8H5S
|
| 331 |
+
8CAR
|
| 332 |
+
8FEH
|
| 333 |
+
8UGC
|
| 334 |
+
8GJY
|
| 335 |
+
8OYX
|
| 336 |
+
8DOT
|
| 337 |
+
8D00
|
| 338 |
+
8B6N
|
| 339 |
+
8ANK
|
| 340 |
+
8ARL
|
| 341 |
+
8POF
|
| 342 |
+
8HCY
|
| 343 |
+
7Y3Z
|
| 344 |
+
8KCA
|
| 345 |
+
8CGM
|
| 346 |
+
7QRL
|
| 347 |
+
8GSY
|
| 348 |
+
8T0J
|
| 349 |
+
8HNA
|
| 350 |
+
8OXL
|
| 351 |
+
8P7A
|
| 352 |
+
8EB9
|
| 353 |
+
8JNM
|
| 354 |
+
8I0A
|
| 355 |
+
8F8N
|
| 356 |
+
8P5P
|
| 357 |
+
8OYS
|
| 358 |
+
8JIY
|
| 359 |
+
7XXE
|
| 360 |
+
7YD4
|
| 361 |
+
8FYG
|
| 362 |
+
8HZZ
|
| 363 |
+
8ES6
|
| 364 |
+
7WU8
|
| 365 |
+
8GJW
|
| 366 |
+
8SOT
|
| 367 |
+
8B4U
|
| 368 |
+
8AY2
|
| 369 |
+
8J1W
|
| 370 |
+
8HM4
|
| 371 |
+
8VEH
|
| 372 |
+
8TMS
|
| 373 |
+
8HX3
|
| 374 |
+
8FJF
|
| 375 |
+
8TVL
|
| 376 |
+
8BKE
|
| 377 |
+
8J69
|
| 378 |
+
8EOV
|
| 379 |
+
8OYW
|
| 380 |
+
8BJV
|
| 381 |
+
7TBO
|
| 382 |
+
8OXH
|
| 383 |
+
8B74
|
| 384 |
+
8ANJ
|
| 385 |
+
8HJJ
|
| 386 |
+
8U0X
|
| 387 |
+
8OYV
|
| 388 |
+
7YJL
|
| 389 |
+
8DHJ
|
| 390 |
+
8EO7
|
| 391 |
+
8JU8
|
| 392 |
+
8JN0
|
| 393 |
+
7VS2
|
| 394 |
+
8F5D
|
| 395 |
+
8ETQ
|
| 396 |
+
8J0H
|
| 397 |
+
8JNN
|
| 398 |
+
8CQ3
|
model/boltzgen/task/__init__.py
ADDED
|
File without changes
|
model/boltzgen/task/analyze/analyze.py
ADDED
|
@@ -0,0 +1,1483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from boltzgen.utils.quiet import quiet_startup
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
quiet_startup()
|
| 5 |
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
| 6 |
+
from concurrent.futures.process import BrokenProcessPool
|
| 7 |
+
import copy
|
| 8 |
+
import multiprocessing
|
| 9 |
+
import numbers
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import traceback
|
| 12 |
+
from typing import Optional, Dict, Any, List
|
| 13 |
+
import subprocess
|
| 14 |
+
import re
|
| 15 |
+
import json
|
| 16 |
+
|
| 17 |
+
from boltzgen.task.analyze.analyze_utils import (
|
| 18 |
+
TARGET_ID_RE,
|
| 19 |
+
calc_hydrophobicity,
|
| 20 |
+
compute_liability_metrics,
|
| 21 |
+
compute_novelty_foldseek,
|
| 22 |
+
compute_rmsd,
|
| 23 |
+
compute_ss_metrics,
|
| 24 |
+
get_best_folding_sample,
|
| 25 |
+
get_delta_sasa,
|
| 26 |
+
get_fold_metrics,
|
| 27 |
+
get_motif_set,
|
| 28 |
+
count_noncovalents,
|
| 29 |
+
largest_hydrophobic_patch_area,
|
| 30 |
+
make_histogram,
|
| 31 |
+
save_design_only_structure_to_cif,
|
| 32 |
+
save_design_only_structure_to_pdb,
|
| 33 |
+
vendi_scores,
|
| 34 |
+
vendi_sequences,
|
| 35 |
+
)
|
| 36 |
+
from matplotlib import pyplot as plt
|
| 37 |
+
import matplotlib as mpl
|
| 38 |
+
|
| 39 |
+
mpl.rcParams["figure.max_open_warning"] = 100
|
| 40 |
+
import pydssp
|
| 41 |
+
import rdkit
|
| 42 |
+
from boltzgen.task.predict.data_from_generated import FromGeneratedDataModule
|
| 43 |
+
|
| 44 |
+
import torch
|
| 45 |
+
import numpy as np
|
| 46 |
+
import pandas as pd
|
| 47 |
+
from tqdm import tqdm
|
| 48 |
+
from collections import defaultdict
|
| 49 |
+
|
| 50 |
+
from boltzgen.data import const
|
| 51 |
+
from boltzgen.task.task import Task
|
| 52 |
+
from boltzgen.data.data import Structure
|
| 53 |
+
from boltzgen.data.write.mmcif import to_mmcif
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class Analyze(Task):
|
| 57 |
+
"""
|
| 58 |
+
The Analyze step of the BoltzGen pipeline.
|
| 59 |
+
It computes various metrics on CPU.
|
| 60 |
+
It aggregates metrics from the Folding, Designfolding, and Affinity predictions step.
|
| 61 |
+
It writes metrics into a single csv file which is then used in the filter task.
|
| 62 |
+
It also creates a pickle file of sequences and structures for similarity assessments during filtering.
|
| 63 |
+
(needed for quality-diversity optimization algorithm)
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
def __init__(
|
| 67 |
+
self,
|
| 68 |
+
name: str,
|
| 69 |
+
data: FromGeneratedDataModule,
|
| 70 |
+
design_dir: str = None,
|
| 71 |
+
backbone_fold_metrics: bool = False,
|
| 72 |
+
allatom_fold_metrics: bool = True,
|
| 73 |
+
affinity_metrics: bool = False,
|
| 74 |
+
noncovalents_original: bool = False,
|
| 75 |
+
noncovalents_refolded: bool = False,
|
| 76 |
+
diversity_original: bool = False,
|
| 77 |
+
diversity_refolded: bool = False,
|
| 78 |
+
diversity_per_target_original: bool = False,
|
| 79 |
+
diversity_per_target_refolded: bool = False,
|
| 80 |
+
novelty_original: bool = False,
|
| 81 |
+
novelty_refolded: bool = False,
|
| 82 |
+
novelty_per_target_original: bool = False,
|
| 83 |
+
novelty_per_target_refolded: bool = False,
|
| 84 |
+
delta_sasa_original: bool = False,
|
| 85 |
+
delta_sasa_refolded: bool = False,
|
| 86 |
+
largest_hydrophobic: bool = False,
|
| 87 |
+
largest_hydrophobic_refolded: bool = False,
|
| 88 |
+
disulfide_quality: bool = False,
|
| 89 |
+
free_cys: bool = False,
|
| 90 |
+
compute_lddts: bool = True, # computing LDDTs takes ~5-15 sec so it is optional
|
| 91 |
+
run_clustering: bool = False,
|
| 92 |
+
native: bool = False,
|
| 93 |
+
sequence_recovery: bool = False,
|
| 94 |
+
ss_conditioning_metrics: bool = False,
|
| 95 |
+
liability_analysis: bool = False,
|
| 96 |
+
liability_modality: str = "antibody",
|
| 97 |
+
liability_peptide_type: str = "linear",
|
| 98 |
+
debug: bool = False,
|
| 99 |
+
wandb: Optional[Dict[str, Any]] = None,
|
| 100 |
+
slurm: bool = False,
|
| 101 |
+
diversity_subset: int = None,
|
| 102 |
+
num_processes: int = 1,
|
| 103 |
+
foldseek_db: str = "/data/rbg/users/hstark/proteinblobs/data/foldseek_pdb/pdb",
|
| 104 |
+
foldseek_binary: str = "/data/rbg/users/hstark/foldseek/bin/foldseek",
|
| 105 |
+
skip_specific_ids: List[str] = None,
|
| 106 |
+
designfolding_metrics: bool = False,
|
| 107 |
+
use_design_mask_for_target: bool = False,
|
| 108 |
+
) -> None:
|
| 109 |
+
"""Initialize the task.
|
| 110 |
+
|
| 111 |
+
Parameters
|
| 112 |
+
----------
|
| 113 |
+
fold_metrics : bool,
|
| 114 |
+
Compute folding metrics and assume that the folding directory exists.
|
| 115 |
+
"""
|
| 116 |
+
super().__init__()
|
| 117 |
+
self.name = name
|
| 118 |
+
self.num_processes = num_processes
|
| 119 |
+
self.foldseek_db = Path(foldseek_db)
|
| 120 |
+
self.foldseek_binary = foldseek_binary
|
| 121 |
+
self.skip_specific_ids = set(skip_specific_ids or [])
|
| 122 |
+
self.data = data
|
| 123 |
+
self.noncovalents_original = noncovalents_original
|
| 124 |
+
self.noncovalents_refolded = noncovalents_refolded
|
| 125 |
+
self.diversity_original = diversity_original
|
| 126 |
+
self.diversity_refolded = diversity_refolded
|
| 127 |
+
self.diversity_per_target_original = diversity_per_target_original
|
| 128 |
+
self.diversity_per_target_refolded = diversity_per_target_refolded
|
| 129 |
+
self.novelty_original = novelty_original
|
| 130 |
+
self.novelty_refolded = novelty_refolded
|
| 131 |
+
self.novelty_per_target_original = novelty_per_target_original
|
| 132 |
+
self.novelty_per_target_refolded = novelty_per_target_refolded
|
| 133 |
+
self.delta_sasa_original = delta_sasa_original
|
| 134 |
+
self.delta_sasa_refolded = delta_sasa_refolded
|
| 135 |
+
self.largest_hydrophobic = largest_hydrophobic
|
| 136 |
+
self.largest_hydrophobic_refolded = largest_hydrophobic_refolded
|
| 137 |
+
self.compute_lddts = compute_lddts
|
| 138 |
+
self.run_clustering = run_clustering
|
| 139 |
+
self.affinity_metrics = affinity_metrics
|
| 140 |
+
self.fold_metrics = backbone_fold_metrics or allatom_fold_metrics
|
| 141 |
+
self.backbone_fold_metrics = backbone_fold_metrics
|
| 142 |
+
self.allatom_fold_metrics = allatom_fold_metrics
|
| 143 |
+
self.designfolding_metrics = designfolding_metrics
|
| 144 |
+
self.disulfide_quality = disulfide_quality
|
| 145 |
+
self.free_cys = free_cys
|
| 146 |
+
self.native = native
|
| 147 |
+
self.sequence_recovery = sequence_recovery
|
| 148 |
+
self.ss_conditioning_metrics = ss_conditioning_metrics
|
| 149 |
+
self.liability_analysis = liability_analysis
|
| 150 |
+
self.liability_modality = liability_modality
|
| 151 |
+
self.liability_peptide_type = liability_peptide_type
|
| 152 |
+
self.debug = debug
|
| 153 |
+
self.wandb = wandb
|
| 154 |
+
self.slurm = slurm
|
| 155 |
+
self.diversity_subset = diversity_subset
|
| 156 |
+
self.use_design_mask_for_target = use_design_mask_for_target
|
| 157 |
+
|
| 158 |
+
# Prevent each worker process from spawning its own multithreaded pools
|
| 159 |
+
torch.set_num_threads(1)
|
| 160 |
+
torch.set_num_interop_threads(1)
|
| 161 |
+
|
| 162 |
+
if design_dir is not None:
|
| 163 |
+
self.init_datasets(design_dir, load_dataset=False)
|
| 164 |
+
|
| 165 |
+
# Check that native structure is available if native metrics are desired
|
| 166 |
+
if self.native and not self.data.return_native:
|
| 167 |
+
msg = "native=True requires return_native=True in data config."
|
| 168 |
+
raise ValueError(msg)
|
| 169 |
+
if self.sequence_recovery and not self.native:
|
| 170 |
+
msg = "sequence_recovery=True requires native structure (native=True)."
|
| 171 |
+
raise ValueError(msg)
|
| 172 |
+
|
| 173 |
+
self.bindsite_adherence_thresholds = [3, 4, 5, 6, 7, 8, 9]
|
| 174 |
+
|
| 175 |
+
def init_datasets(self, design_dir: str, load_dataset: bool = False):
|
| 176 |
+
self.design_dir = Path(design_dir)
|
| 177 |
+
|
| 178 |
+
if load_dataset:
|
| 179 |
+
self.data.init_dataset(design_dir, skip_specific_ids=self.skip_specific_ids)
|
| 180 |
+
|
| 181 |
+
self.des_pdb_dir = self.design_dir / "des_pdbs"
|
| 182 |
+
self.des_pdb_dir.mkdir(parents=True, exist_ok=True)
|
| 183 |
+
self.des_refold_pdb_dir = self.design_dir / "des_refold_pdbs"
|
| 184 |
+
self.des_refold_pdb_dir.mkdir(parents=True, exist_ok=True)
|
| 185 |
+
self.refold_cif_dir = self.design_dir / const.refold_cif_dirname
|
| 186 |
+
self.refold_cif_dir.mkdir(parents=True, exist_ok=True)
|
| 187 |
+
self.metrics_dir = self.design_dir / const.metrics_dirname
|
| 188 |
+
self.metrics_dir.mkdir(exist_ok=True, parents=True)
|
| 189 |
+
|
| 190 |
+
def run_parallel(self, num, num_processes):
|
| 191 |
+
"""
|
| 192 |
+
Run tasks in parallel. If a worker crashes and the pool breaks,
|
| 193 |
+
restart a fresh pool and only rerun tasks that truly didn't finish.
|
| 194 |
+
"""
|
| 195 |
+
ctx = multiprocessing.get_context("spawn")
|
| 196 |
+
|
| 197 |
+
all_task_ids = set(range(num))
|
| 198 |
+
completed_task_ids = set()
|
| 199 |
+
sample_ids = []
|
| 200 |
+
|
| 201 |
+
pbar = tqdm(total=num, desc="Processing samples")
|
| 202 |
+
|
| 203 |
+
while completed_task_ids != all_task_ids:
|
| 204 |
+
remaining = sorted(all_task_ids - completed_task_ids)
|
| 205 |
+
|
| 206 |
+
try:
|
| 207 |
+
with ProcessPoolExecutor(
|
| 208 |
+
max_workers=num_processes, mp_context=ctx
|
| 209 |
+
) as ex:
|
| 210 |
+
fut2idx = {ex.submit(self.compute_metrics, i): i for i in remaining}
|
| 211 |
+
|
| 212 |
+
# Iterate over futures that actually *completed* (finished or raised)
|
| 213 |
+
for f in as_completed(fut2idx):
|
| 214 |
+
idx = fut2idx[f]
|
| 215 |
+
try:
|
| 216 |
+
sid = f.result()
|
| 217 |
+
if sid is not None:
|
| 218 |
+
sample_ids.append(sid)
|
| 219 |
+
# Count successful completion
|
| 220 |
+
completed_task_ids.add(idx)
|
| 221 |
+
pbar.update(1)
|
| 222 |
+
|
| 223 |
+
except BrokenProcessPool:
|
| 224 |
+
# Pool is dead, mark this idx completed.
|
| 225 |
+
# Let the outer except restart a fresh pool for all unfinished.
|
| 226 |
+
completed_task_ids.add(idx)
|
| 227 |
+
pbar.update(1)
|
| 228 |
+
raise
|
| 229 |
+
|
| 230 |
+
except BrokenProcessPool:
|
| 231 |
+
print("\nPOOL BROKEN: A worker died. Restarting with remaining tasks…")
|
| 232 |
+
# Nothing else to do: the while-loop will retry only the unfinished tasks.
|
| 233 |
+
continue
|
| 234 |
+
|
| 235 |
+
pbar.close()
|
| 236 |
+
return sample_ids
|
| 237 |
+
|
| 238 |
+
def run(self, config=None, run_prediction=False):
|
| 239 |
+
self.distribute_tasks()
|
| 240 |
+
self.aggregate_metrics()
|
| 241 |
+
|
| 242 |
+
def distribute_tasks(self):
|
| 243 |
+
# The rdkit thing is necessary to make multiprocessing with the rdkit molecules work.
|
| 244 |
+
rdkit.Chem.SetDefaultPickleProperties(rdkit.Chem.PropertyPickleOptions.AllProps)
|
| 245 |
+
|
| 246 |
+
# Compute metrics and write them to disk
|
| 247 |
+
sample_ids = []
|
| 248 |
+
num = len(self.data.predict_set)
|
| 249 |
+
if num == 0:
|
| 250 |
+
msg = "There were 0 samples to compute metrics for. Skipping the distribute_tasks step that calls compute_metrics"
|
| 251 |
+
print(msg)
|
| 252 |
+
return
|
| 253 |
+
num_processes = min(self.num_processes, multiprocessing.cpu_count())
|
| 254 |
+
if num_processes == 1:
|
| 255 |
+
for idx in tqdm(range(num)):
|
| 256 |
+
sample_id = self.compute_metrics(idx)
|
| 257 |
+
if sample_id is not None:
|
| 258 |
+
sample_ids.append(sample_id)
|
| 259 |
+
else:
|
| 260 |
+
sample_ids = self.run_parallel(num, num_processes)
|
| 261 |
+
print(f"Computed metrics successfully for {len(sample_ids)} out of {num}.")
|
| 262 |
+
|
| 263 |
+
def aggregate_metrics(self):
|
| 264 |
+
# Load and aggregate saved metrics from disk
|
| 265 |
+
|
| 266 |
+
# Collect sample IDs for data_*.npz and metrics_*.npz
|
| 267 |
+
data_ids = {
|
| 268 |
+
f.stem.replace("data_", "") for f in self.metrics_dir.glob("data_*.npz")
|
| 269 |
+
}
|
| 270 |
+
metrics_ids = {
|
| 271 |
+
f.stem.replace("metrics_", "")
|
| 272 |
+
for f in self.metrics_dir.glob("metrics_*.npz")
|
| 273 |
+
}
|
| 274 |
+
sample_ids = sorted(data_ids & metrics_ids)
|
| 275 |
+
assert len(sample_ids) > 0
|
| 276 |
+
|
| 277 |
+
all_metrics, all_data = [], []
|
| 278 |
+
for sample_id in tqdm(
|
| 279 |
+
sample_ids, desc=f"Loading saved metrics from disk. 1% of total"
|
| 280 |
+
):
|
| 281 |
+
data = np.load(
|
| 282 |
+
self.metrics_dir / f"data_{sample_id}.npz", allow_pickle=True
|
| 283 |
+
)
|
| 284 |
+
metrics = np.load(
|
| 285 |
+
self.metrics_dir / f"metrics_{sample_id}.npz", allow_pickle=True
|
| 286 |
+
)
|
| 287 |
+
data = {
|
| 288 |
+
k: v.item() if v.shape == () else torch.tensor(v)
|
| 289 |
+
for k, v in data.items()
|
| 290 |
+
}
|
| 291 |
+
metrics = {
|
| 292 |
+
k: v.item() if v.shape == () else torch.tensor(v)
|
| 293 |
+
for k, v in metrics.items()
|
| 294 |
+
}
|
| 295 |
+
all_metrics.append(metrics)
|
| 296 |
+
all_data.append(data)
|
| 297 |
+
df = pd.DataFrame(all_metrics)
|
| 298 |
+
|
| 299 |
+
# Cast per-motif integer fields and reconstruct a consolidated details column
|
| 300 |
+
try:
|
| 301 |
+
motif_keys = list(
|
| 302 |
+
get_motif_set(
|
| 303 |
+
modality=self.liability_modality,
|
| 304 |
+
peptide_type=self.liability_peptide_type,
|
| 305 |
+
).keys()
|
| 306 |
+
)
|
| 307 |
+
except Exception:
|
| 308 |
+
motif_keys = []
|
| 309 |
+
|
| 310 |
+
# Build a consolidated details string if components exist
|
| 311 |
+
details_cols = []
|
| 312 |
+
for motif in motif_keys:
|
| 313 |
+
pos_col = f"liability_{motif}_position"
|
| 314 |
+
len_col = f"liability_{motif}_length"
|
| 315 |
+
sev_col = f"liability_{motif}_severity"
|
| 316 |
+
det_col = f"liability_{motif}_details"
|
| 317 |
+
cnt_col = f"liability_{motif}_count"
|
| 318 |
+
numpos_col = f"liability_{motif}_num_positions"
|
| 319 |
+
# Standardize dtypes: fill NA for ints, then cast
|
| 320 |
+
for col in [pos_col, len_col, sev_col, cnt_col, numpos_col]:
|
| 321 |
+
if col in df.columns:
|
| 322 |
+
if df[col].dtype.kind in ("f", "O"):
|
| 323 |
+
df[col] = df[col].fillna(-1).astype(int)
|
| 324 |
+
# Keep details as string
|
| 325 |
+
if det_col in df.columns:
|
| 326 |
+
df[det_col] = df[det_col].fillna("").astype(str)
|
| 327 |
+
# Track columns for a consolidated details view
|
| 328 |
+
if all(
|
| 329 |
+
c in df.columns for c in [pos_col, len_col, sev_col, det_col, cnt_col]
|
| 330 |
+
):
|
| 331 |
+
details_cols.append(
|
| 332 |
+
(motif, pos_col, len_col, sev_col, det_col, cnt_col)
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
# Optional: single consolidated details column combining motifs
|
| 336 |
+
if details_cols:
|
| 337 |
+
|
| 338 |
+
def _compose_details(row):
|
| 339 |
+
items = []
|
| 340 |
+
for motif, pos_col, len_col, sev_col, det_col, cnt_col in details_cols:
|
| 341 |
+
cnt = row[cnt_col]
|
| 342 |
+
if cnt and cnt > 0:
|
| 343 |
+
pos = row[pos_col]
|
| 344 |
+
length = row[len_col]
|
| 345 |
+
sev = row[sev_col]
|
| 346 |
+
det = row[det_col]
|
| 347 |
+
base = f"{motif}x{cnt}"
|
| 348 |
+
if pos >= 0:
|
| 349 |
+
base += f"(pos{pos},len{length},sev{sev})"
|
| 350 |
+
else:
|
| 351 |
+
base += f"(sev{sev})"
|
| 352 |
+
if det:
|
| 353 |
+
base += f"[{det}]"
|
| 354 |
+
items.append(base)
|
| 355 |
+
return "; ".join(items)
|
| 356 |
+
|
| 357 |
+
df["liability_details"] = df.apply(_compose_details, axis=1)
|
| 358 |
+
|
| 359 |
+
# Run clustering
|
| 360 |
+
if self.run_clustering:
|
| 361 |
+
df = self.run_foldseek_clustering(df)
|
| 362 |
+
# Write individual metrics to disk
|
| 363 |
+
csv_path = Path(self.design_dir) / f"aggregate_metrics_{self.name}.csv"
|
| 364 |
+
df.to_csv(csv_path, float_format="%.5f", index=False)
|
| 365 |
+
|
| 366 |
+
# Store ca coords and seq in a pickle file for later usage in e.g. diversity aware filtering
|
| 367 |
+
data_rows = []
|
| 368 |
+
for data in all_data:
|
| 369 |
+
data_rows.append(
|
| 370 |
+
{
|
| 371 |
+
"id": data["sample_id"],
|
| 372 |
+
"target_id": data["target_id"],
|
| 373 |
+
"sequence": "".join(
|
| 374 |
+
[
|
| 375 |
+
const.prot_token_to_letter[const.tokens[t]]
|
| 376 |
+
for t in data["design_seq"]
|
| 377 |
+
]
|
| 378 |
+
),
|
| 379 |
+
"ca_coords": json.dumps(data["ca_coords"].numpy().tolist()),
|
| 380 |
+
}
|
| 381 |
+
)
|
| 382 |
+
ca_seq_df = pd.DataFrame(data_rows)
|
| 383 |
+
ca_seq_df.to_pickle(
|
| 384 |
+
Path(self.design_dir) / "ca_coords_sequences.pkl.gz", compression="gzip"
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
# Compute per target metrics
|
| 388 |
+
df["target_id"] = df["id"].apply(lambda s: TARGET_ID_RE.match(s).group(1))
|
| 389 |
+
per_target_df = df.groupby("target_id").mean(numeric_only=True).reset_index()
|
| 390 |
+
csv_path = Path(self.design_dir) / f"per_target_metrics_{self.name}.csv"
|
| 391 |
+
per_target_df.to_csv(csv_path, float_format="%.5f", index=False)
|
| 392 |
+
|
| 393 |
+
avg_metrics = df.mean(numeric_only=True).round(5).to_dict()
|
| 394 |
+
avg_metrics["num_targets"] = len(all_metrics)
|
| 395 |
+
if self.run_clustering:
|
| 396 |
+
if "cluster_07_seqidentity" in df.columns:
|
| 397 |
+
avg_metrics["num_cluster_07_seqidentity"] = len(
|
| 398 |
+
np.unique(df["cluster_07_seqidentity"].to_numpy())
|
| 399 |
+
)
|
| 400 |
+
if "num_clusters_05_tmscore" in df.columns:
|
| 401 |
+
avg_metrics["num_clusters_05_tmscore"] = len(
|
| 402 |
+
np.unique(df["clusters_05_tmscore"].to_numpy())
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
diversity_metrics, diversity_data = self.compute_diversity(
|
| 406 |
+
all_data, all_metrics
|
| 407 |
+
)
|
| 408 |
+
for k in diversity_metrics:
|
| 409 |
+
avg_metrics[k] = diversity_metrics[k]
|
| 410 |
+
|
| 411 |
+
novelty_metrics, novelty_data = self.compute_novelty()
|
| 412 |
+
for k in novelty_metrics:
|
| 413 |
+
avg_metrics[k] = novelty_metrics[k]
|
| 414 |
+
|
| 415 |
+
_, histograms = self.make_histograms(all_metrics)
|
| 416 |
+
|
| 417 |
+
# Log to Wandb
|
| 418 |
+
if self.wandb is not None and not self.debug:
|
| 419 |
+
import wandb
|
| 420 |
+
|
| 421 |
+
print("\nOverall average metrics:", avg_metrics)
|
| 422 |
+
|
| 423 |
+
# Make residue distribution plot
|
| 424 |
+
native_stats = np.load("data/native_statistics.npz")
|
| 425 |
+
design_freqs = np.array(
|
| 426 |
+
[
|
| 427 |
+
avg_metrics[f"{k}_fraction"]
|
| 428 |
+
for k in const.fake_atom_placements.keys()
|
| 429 |
+
]
|
| 430 |
+
)
|
| 431 |
+
x = np.arange(len(const.fake_atom_placements.keys()))
|
| 432 |
+
width = 0.15
|
| 433 |
+
fig_res, ax = plt.subplots(figsize=(12, 6))
|
| 434 |
+
ax.bar(x - width / 2, design_freqs, width, label="Design frequency")
|
| 435 |
+
ax.bar(
|
| 436 |
+
x + width / 2, native_stats["res_dist"], width, label="Data frequency"
|
| 437 |
+
)
|
| 438 |
+
ax.set_xlabel("Res Type")
|
| 439 |
+
ax.set_ylabel("Probability")
|
| 440 |
+
ax.set_title("Res Type distributions")
|
| 441 |
+
ax.set_xticks(x)
|
| 442 |
+
ax.set_xticklabels(const.fake_atom_placements.keys())
|
| 443 |
+
ax.legend()
|
| 444 |
+
ax.grid(True, which="both", axis="y", linestyle="--", linewidth=0.5)
|
| 445 |
+
plt.tight_layout()
|
| 446 |
+
fig_res.savefig(Path(self.design_dir) / "res_type_distribution.png")
|
| 447 |
+
|
| 448 |
+
# Make secondary structure distribution plot
|
| 449 |
+
ss_dist = np.array(
|
| 450 |
+
[avg_metrics["loop"], avg_metrics["helix"], avg_metrics["sheet"]]
|
| 451 |
+
)
|
| 452 |
+
x = np.arange(3)
|
| 453 |
+
width = 0.15
|
| 454 |
+
fig_ss, ax = plt.subplots(figsize=(12, 6))
|
| 455 |
+
ax.bar(x - width / 2, ss_dist, width, label="Designed")
|
| 456 |
+
ax.bar(x + width / 2, native_stats["ss_dist"], width, label="Native data")
|
| 457 |
+
ax.set_xlabel("Secondary Structure type")
|
| 458 |
+
ax.set_ylabel("Frequency")
|
| 459 |
+
ax.set_title("Secondary Structure distributions")
|
| 460 |
+
ax.set_xticks(x)
|
| 461 |
+
ax.set_xticklabels(["loop", "helix", "sheet"])
|
| 462 |
+
ax.legend()
|
| 463 |
+
ax.grid(True, which="both", axis="y", linestyle="--", linewidth=0.5)
|
| 464 |
+
plt.tight_layout()
|
| 465 |
+
fig_ss.savefig(
|
| 466 |
+
Path(self.design_dir) / "secondary_structure_distribution.png"
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
wandb.init(name=self.name, **self.wandb)
|
| 470 |
+
wandb.log(avg_metrics)
|
| 471 |
+
wandb.log({"res_dist": wandb.Image(fig_res)})
|
| 472 |
+
wandb.log({"ss_dist": wandb.Image(fig_ss)})
|
| 473 |
+
|
| 474 |
+
# Log histograms
|
| 475 |
+
for name, fig in histograms.items():
|
| 476 |
+
wandb.log({f"{name}_hist": wandb.Image(fig)})
|
| 477 |
+
|
| 478 |
+
# plot per target vendiscore histogram
|
| 479 |
+
if self.diversity_per_target_original:
|
| 480 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 481 |
+
ax.hist(
|
| 482 |
+
diversity_data["vendi_tm_fixed"], bins=50, color="blue", alpha=0.7
|
| 483 |
+
)
|
| 484 |
+
ax.set_title("Vendi Score Per Target Distribution")
|
| 485 |
+
ax.set_xlabel("Vendi Score")
|
| 486 |
+
ax.set_ylabel("Count")
|
| 487 |
+
plt.tight_layout()
|
| 488 |
+
wandb.log({"vendi_per_target": wandb.Image(fig)})
|
| 489 |
+
plt.close(fig)
|
| 490 |
+
|
| 491 |
+
if self.novelty_per_target_original:
|
| 492 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 493 |
+
ax.hist(
|
| 494 |
+
novelty_data["nov_df"]["novelty"],
|
| 495 |
+
bins=50,
|
| 496 |
+
color="blue",
|
| 497 |
+
alpha=0.7,
|
| 498 |
+
)
|
| 499 |
+
ax.set_title("Novelty Per Target Original Distribution")
|
| 500 |
+
ax.set_xlabel("Novelty")
|
| 501 |
+
ax.set_ylabel("Count")
|
| 502 |
+
plt.tight_layout()
|
| 503 |
+
wandb.log({"novelty_per_target_original_hist": wandb.Image(fig)})
|
| 504 |
+
plt.close(fig)
|
| 505 |
+
|
| 506 |
+
if self.novelty_per_target_refolded and (self.fold_metrics):
|
| 507 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 508 |
+
ax.hist(
|
| 509 |
+
novelty_data["nov_df_refold"]["novelty"],
|
| 510 |
+
bins=50,
|
| 511 |
+
color="blue",
|
| 512 |
+
alpha=0.7,
|
| 513 |
+
)
|
| 514 |
+
ax.set_title("Novelty Per Target Refolded Distribution")
|
| 515 |
+
ax.set_xlabel("Novelty")
|
| 516 |
+
ax.set_ylabel("Count")
|
| 517 |
+
plt.tight_layout()
|
| 518 |
+
wandb.log({"novelty_per_target_refolded_hist": wandb.Image(fig)})
|
| 519 |
+
plt.close(fig)
|
| 520 |
+
|
| 521 |
+
def compute_metrics(self, idx=None, sample_id=None, suffix=None, design_dir=None):
|
| 522 |
+
if sample_id is None:
|
| 523 |
+
sample_id = self.data.predict_set.generated_paths[idx].stem
|
| 524 |
+
feat = self.data.predict_set[idx]
|
| 525 |
+
else:
|
| 526 |
+
if design_dir is None:
|
| 527 |
+
design_dir = self.design_dir
|
| 528 |
+
|
| 529 |
+
feat = self.data.predict_set.get_sample(
|
| 530 |
+
design_dir=design_dir, sample_id=sample_id
|
| 531 |
+
)
|
| 532 |
+
path = feat["path"]
|
| 533 |
+
if feat["exception"]:
|
| 534 |
+
msg = f"Failed obtaining valid features for {path} due to feat['exception']. Skipping."
|
| 535 |
+
print(msg)
|
| 536 |
+
return None
|
| 537 |
+
|
| 538 |
+
# Get designed sequence
|
| 539 |
+
res_type_argmax = torch.argmax(feat["res_type"], dim=-1)
|
| 540 |
+
design_seq_tensor = res_type_argmax[
|
| 541 |
+
feat["design_mask"].bool() & feat["token_pad_mask"].bool()
|
| 542 |
+
]
|
| 543 |
+
design_chain_id = feat["asym_id"][
|
| 544 |
+
torch.where(feat["design_mask"].bool() & feat["token_pad_mask"].bool())[0][
|
| 545 |
+
0
|
| 546 |
+
]
|
| 547 |
+
].item()
|
| 548 |
+
design_chain_seq = res_type_argmax[design_chain_id == feat["asym_id"]]
|
| 549 |
+
design_seq = "".join(
|
| 550 |
+
[
|
| 551 |
+
const.prot_token_to_letter.get(const.tokens[t], "X")
|
| 552 |
+
for t in design_seq_tensor
|
| 553 |
+
]
|
| 554 |
+
)
|
| 555 |
+
design_chain_seq = "".join(
|
| 556 |
+
[
|
| 557 |
+
const.prot_token_to_letter.get(const.tokens[t], "X")
|
| 558 |
+
for t in design_chain_seq
|
| 559 |
+
]
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
# initialize metrics
|
| 563 |
+
metrics = {
|
| 564 |
+
"id": sample_id,
|
| 565 |
+
"file_name": path.name,
|
| 566 |
+
"designed_sequence": design_seq,
|
| 567 |
+
"designed_chain_sequence": design_chain_seq,
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
# Add per-chain sequences to csv when designing multiple chains
|
| 571 |
+
design_token_indices = torch.where(feat["design_mask"].bool() & feat["token_pad_mask"].bool())[0]
|
| 572 |
+
designed_chain_ids = feat["asym_id"][design_token_indices].unique().tolist()
|
| 573 |
+
if len(designed_chain_ids) > 1:
|
| 574 |
+
for chain_id in designed_chain_ids:
|
| 575 |
+
chain_mask = feat["asym_id"] == chain_id
|
| 576 |
+
|
| 577 |
+
# Full chain sequence
|
| 578 |
+
chain_res_types = res_type_argmax[chain_mask]
|
| 579 |
+
full_chain_seq = "".join(
|
| 580 |
+
[
|
| 581 |
+
const.prot_token_to_letter.get(const.tokens[t], "X")
|
| 582 |
+
for t in chain_res_types
|
| 583 |
+
]
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
# Designed residues only from this chain
|
| 587 |
+
design_chain_mask = feat["design_mask"].bool() & feat["token_pad_mask"].bool() & chain_mask
|
| 588 |
+
design_res_types = res_type_argmax[design_chain_mask]
|
| 589 |
+
design_seq = "".join(
|
| 590 |
+
[
|
| 591 |
+
const.prot_token_to_letter.get(const.tokens[t], "X")
|
| 592 |
+
for t in design_res_types
|
| 593 |
+
]
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
metrics[f"designed_sequence_{chain_id}"] = design_seq
|
| 597 |
+
metrics[f"full_sequence_{chain_id}"] = full_chain_seq
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
target_id = re.search(rf"{self.data.cfg.target_id_regex}", sample_id).group(1)
|
| 601 |
+
|
| 602 |
+
# Get masks
|
| 603 |
+
design_mask = feat["design_mask"].bool()
|
| 604 |
+
chain_design_mask = feat["chain_design_mask"].bool()
|
| 605 |
+
|
| 606 |
+
design_resolved_mask = design_mask & feat["token_resolved_mask"].bool()
|
| 607 |
+
|
| 608 |
+
# For symmetric designs where all chains have designed residues, use design_mask
|
| 609 |
+
# instead of chain_design_mask so "target" = non-designed residues (not empty)
|
| 610 |
+
if self.use_design_mask_for_target:
|
| 611 |
+
target_resolved_mask = (~design_mask) & feat["token_resolved_mask"].bool()
|
| 612 |
+
else:
|
| 613 |
+
target_resolved_mask = (~chain_design_mask) & feat["token_resolved_mask"].bool()
|
| 614 |
+
atom_design_resolved_mask = (
|
| 615 |
+
(feat["atom_to_token"].float() @ design_resolved_mask.unsqueeze(-1).float())
|
| 616 |
+
.bool()
|
| 617 |
+
.squeeze()
|
| 618 |
+
)
|
| 619 |
+
atom_target_resolved_mask = (
|
| 620 |
+
(feat["atom_to_token"].float() @ target_resolved_mask.unsqueeze(-1).float())
|
| 621 |
+
.bool()
|
| 622 |
+
.squeeze()
|
| 623 |
+
)
|
| 624 |
+
atom_resolved_mask = feat["atom_resolved_mask"]
|
| 625 |
+
resolved_atoms_design_mask = atom_design_resolved_mask[atom_resolved_mask]
|
| 626 |
+
resolved_atoms_target_mask = atom_target_resolved_mask[atom_resolved_mask]
|
| 627 |
+
atom_chain_mask = (
|
| 628 |
+
(
|
| 629 |
+
feat["atom_to_token"].float()
|
| 630 |
+
@ chain_design_mask.unsqueeze(-1).float()
|
| 631 |
+
)
|
| 632 |
+
.bool()
|
| 633 |
+
.squeeze()
|
| 634 |
+
)
|
| 635 |
+
|
| 636 |
+
# Get masks for native structure
|
| 637 |
+
if self.native:
|
| 638 |
+
native_design_mask = feat["native_design_mask"].bool()
|
| 639 |
+
native_target_resolved_mask = (
|
| 640 |
+
~native_design_mask & feat["native_token_resolved_mask"].bool()
|
| 641 |
+
)
|
| 642 |
+
native_atom_target_resolved_mask = (
|
| 643 |
+
(
|
| 644 |
+
feat["native_atom_to_token"].float()
|
| 645 |
+
@ native_target_resolved_mask.unsqueeze(-1).float()
|
| 646 |
+
)
|
| 647 |
+
.bool()
|
| 648 |
+
.squeeze()
|
| 649 |
+
)
|
| 650 |
+
|
| 651 |
+
# add to design_only directory for novelty computation
|
| 652 |
+
des_cif_path = None
|
| 653 |
+
if not suffix is None:
|
| 654 |
+
des_pdb_dir = self.des_pdb_dir / suffix
|
| 655 |
+
des_pdb_dir.mkdir(exist_ok=True, parents=True)
|
| 656 |
+
else:
|
| 657 |
+
des_pdb_dir = self.des_pdb_dir
|
| 658 |
+
des_pdb_path = des_pdb_dir / f"{feat['id']}_des.pdb"
|
| 659 |
+
des_cif_path = des_pdb_path.with_suffix(".cif")
|
| 660 |
+
if (
|
| 661 |
+
self.novelty_original
|
| 662 |
+
or self.novelty_refolded
|
| 663 |
+
or self.novelty_per_target_original
|
| 664 |
+
or self.novelty_per_target_refolded
|
| 665 |
+
or self.run_clustering
|
| 666 |
+
):
|
| 667 |
+
try:
|
| 668 |
+
save_design_only_structure_to_pdb(
|
| 669 |
+
atom_design_mask=atom_chain_mask,
|
| 670 |
+
structure=feat["str_gen"],
|
| 671 |
+
output_path=des_pdb_path,
|
| 672 |
+
)
|
| 673 |
+
des_cif_path = des_pdb_path.with_suffix(".cif")
|
| 674 |
+
except Exception as e:
|
| 675 |
+
print(
|
| 676 |
+
f"[Warning] Could not save design-only structure for {feat['id']}: {e}. Skipping this file."
|
| 677 |
+
)
|
| 678 |
+
traceback.print_exc()
|
| 679 |
+
return None
|
| 680 |
+
|
| 681 |
+
# largest hydrophobic patch area original
|
| 682 |
+
if self.largest_hydrophobic:
|
| 683 |
+
if not des_cif_path.exists():
|
| 684 |
+
save_design_only_structure_to_cif(
|
| 685 |
+
atom_design_mask=atom_chain_mask,
|
| 686 |
+
structure=feat["str_gen"],
|
| 687 |
+
output_path=des_cif_path,
|
| 688 |
+
)
|
| 689 |
+
area = largest_hydrophobic_patch_area(des_cif_path)
|
| 690 |
+
metrics["design_largest_hydrophobic_patch"] = area
|
| 691 |
+
if des_cif_path is not None:
|
| 692 |
+
des_cif_path.unlink(missing_ok=True)
|
| 693 |
+
|
| 694 |
+
# Count logging
|
| 695 |
+
metrics["num_prot_tokens"] = (
|
| 696 |
+
(feat["mol_type"] == const.chain_type_ids["PROTEIN"]).sum().item()
|
| 697 |
+
)
|
| 698 |
+
metrics["num_lig_atoms"] = (
|
| 699 |
+
(feat["mol_type"] == const.chain_type_ids["NONPOLYMER"]).sum().item()
|
| 700 |
+
)
|
| 701 |
+
metrics["num_resolved_tokens"] = feat["token_resolved_mask"].sum().item()
|
| 702 |
+
metrics["num_tokens"] = feat["token_pad_mask"].sum().item()
|
| 703 |
+
metrics["num_design"] = feat["design_mask"].sum().item()
|
| 704 |
+
|
| 705 |
+
# delta sasa for original
|
| 706 |
+
if self.delta_sasa_original:
|
| 707 |
+
(
|
| 708 |
+
delta_sasa_orig,
|
| 709 |
+
design_sasa_unbound,
|
| 710 |
+
design_sasa_bound,
|
| 711 |
+
) = get_delta_sasa(
|
| 712 |
+
path,
|
| 713 |
+
atom_target_mask=resolved_atoms_target_mask,
|
| 714 |
+
atom_design_mask=resolved_atoms_design_mask,
|
| 715 |
+
)
|
| 716 |
+
metrics["delta_sasa_original"] = delta_sasa_orig
|
| 717 |
+
metrics["design_sasa_unbound_original"] = design_sasa_unbound
|
| 718 |
+
metrics["design_sasa_bound_original"] = design_sasa_bound
|
| 719 |
+
|
| 720 |
+
# Noncovalents metrics for original
|
| 721 |
+
try:
|
| 722 |
+
if self.noncovalents_original:
|
| 723 |
+
metrics.update(count_noncovalents(feat))
|
| 724 |
+
except Exception as e:
|
| 725 |
+
print(
|
| 726 |
+
f"[Error] computing noncovalents for {path}: {e}. Skipping this file."
|
| 727 |
+
)
|
| 728 |
+
traceback.print_exc()
|
| 729 |
+
return None
|
| 730 |
+
|
| 731 |
+
# Sequence metrics
|
| 732 |
+
if self.sequence_recovery:
|
| 733 |
+
native_seq = torch.argmax(feat["native_res_type"], dim=-1)[
|
| 734 |
+
native_design_mask
|
| 735 |
+
]
|
| 736 |
+
metrics["seq_recovery"] = (
|
| 737 |
+
(design_seq_tensor == native_seq).float().mean().item()
|
| 738 |
+
)
|
| 739 |
+
for t in const.fake_atom_placements.keys():
|
| 740 |
+
metrics[f"{t}_fraction"] = (
|
| 741 |
+
(design_seq_tensor == const.token_ids[t]).float().mean().item()
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
# Secondary structure metrics
|
| 745 |
+
# Compute secondary structure distribution. First get backbone then use pydssp to compute.
|
| 746 |
+
bb_design_mask = (
|
| 747 |
+
feat["atom_pad_mask"].bool()
|
| 748 |
+
& atom_design_resolved_mask
|
| 749 |
+
& feat["backbone_mask"].bool()
|
| 750 |
+
)
|
| 751 |
+
bb_coords = feat["coords"][0][bb_design_mask]
|
| 752 |
+
num_atoms = bb_coords.shape[0]
|
| 753 |
+
if num_atoms % 4 != 0:
|
| 754 |
+
msg = f"BB atoms {num_atoms} is not divisible by 4 for {path}"
|
| 755 |
+
print(msg)
|
| 756 |
+
traceback.print_exc()
|
| 757 |
+
return None
|
| 758 |
+
bb = bb_coords.reshape(-1, 4, 3)
|
| 759 |
+
ca_coords = bb[:, 1, :]
|
| 760 |
+
if len(bb) > 5:
|
| 761 |
+
try:
|
| 762 |
+
dssp = (
|
| 763 |
+
torch.zeros(bb.shape[0], dtype=torch.long)
|
| 764 |
+
if torch.sum(bb_design_mask).item() == 0
|
| 765 |
+
else pydssp.assign(bb, out_type="index")
|
| 766 |
+
)
|
| 767 |
+
# Secondary structure conditioning metric
|
| 768 |
+
if self.ss_conditioning_metrics:
|
| 769 |
+
ss_conditioning_metricsed = feat["ss_type"][design_mask]
|
| 770 |
+
dssp_adjusted = dssp + 1
|
| 771 |
+
ss_metrics = compute_ss_metrics(
|
| 772 |
+
dssp_adjusted, ss_conditioning_metricsed
|
| 773 |
+
)
|
| 774 |
+
metrics.update(ss_metrics)
|
| 775 |
+
metrics["loop"] = (dssp == 0).float().mean().item()
|
| 776 |
+
metrics["helix"] = (dssp == 1).float().mean().item()
|
| 777 |
+
metrics["sheet"] = (dssp == 2).float().mean().item()
|
| 778 |
+
except:
|
| 779 |
+
traceback.print_exc()
|
| 780 |
+
print(f"DSSP failed for {path}.")
|
| 781 |
+
return None
|
| 782 |
+
else:
|
| 783 |
+
metrics["loop"] = float("nan")
|
| 784 |
+
metrics["helix"] = float("nan")
|
| 785 |
+
metrics["sheet"] = float("nan")
|
| 786 |
+
|
| 787 |
+
# Liability analysis
|
| 788 |
+
if self.liability_analysis:
|
| 789 |
+
try:
|
| 790 |
+
liability_metrics = compute_liability_metrics(
|
| 791 |
+
design_chain_seq,
|
| 792 |
+
self.liability_modality,
|
| 793 |
+
self.liability_peptide_type,
|
| 794 |
+
)
|
| 795 |
+
metrics.update(liability_metrics)
|
| 796 |
+
except Exception as e:
|
| 797 |
+
traceback.print_exc()
|
| 798 |
+
print(f"Liability analysis failed for {sample_id}: {e}")
|
| 799 |
+
return None
|
| 800 |
+
|
| 801 |
+
# Compute RMSD between native (input) and generated conditioning structures.
|
| 802 |
+
# conditioning structure does not have fake atoms, just parse the coordinates and compute rmsd.
|
| 803 |
+
metrics["native_rmsd"] = 0.0
|
| 804 |
+
metrics["native_rmsd_bb"] = 0.0
|
| 805 |
+
if self.native:
|
| 806 |
+
target_coords = feat["coords"][:, atom_target_resolved_mask]
|
| 807 |
+
native_target_coords = feat["native_coords"][
|
| 808 |
+
:, native_atom_target_resolved_mask
|
| 809 |
+
]
|
| 810 |
+
target_rmsd = compute_rmsd(native_target_coords, target_coords)
|
| 811 |
+
|
| 812 |
+
bb_target_coords = feat["coords"][
|
| 813 |
+
:, atom_target_resolved_mask & feat["backbone_mask"].bool()
|
| 814 |
+
]
|
| 815 |
+
bb_native_target_coords = feat["native_coords"][
|
| 816 |
+
:,
|
| 817 |
+
native_atom_target_resolved_mask & feat["native_backbone_mask"].bool(),
|
| 818 |
+
]
|
| 819 |
+
bb_target_rmsd = compute_rmsd(bb_native_target_coords, bb_target_coords)
|
| 820 |
+
|
| 821 |
+
bb_coords = feat["coords"][:, feat["backbone_mask"].bool()]
|
| 822 |
+
bb_native_coords = feat["native_coords"][
|
| 823 |
+
:, feat["native_backbone_mask"].bool()
|
| 824 |
+
]
|
| 825 |
+
bb_rmsd = compute_rmsd(bb_native_coords, bb_coords)
|
| 826 |
+
metrics["native_rmsd_all_bb"] = bb_rmsd.item()
|
| 827 |
+
|
| 828 |
+
metrics["native_rmsd"] = target_rmsd.item()
|
| 829 |
+
metrics["native_rmsd_bb"] = bb_target_rmsd.item()
|
| 830 |
+
|
| 831 |
+
# Check binding site adherence. For each binding site token, find closest design token
|
| 832 |
+
binding_site_mask = feat["binding_type"] == 1
|
| 833 |
+
if binding_site_mask.sum() > 1:
|
| 834 |
+
token_distances = torch.cdist(feat["center_coords"], feat["center_coords"])
|
| 835 |
+
bindsite_design_distances = token_distances[binding_site_mask][
|
| 836 |
+
:, feat["design_mask"]
|
| 837 |
+
]
|
| 838 |
+
min_bindsite_design_distances = bindsite_design_distances.min(axis=1).values
|
| 839 |
+
for threshold in self.bindsite_adherence_thresholds:
|
| 840 |
+
metrics[f"bindsite_under_{threshold}rmsd"] = (
|
| 841 |
+
(min_bindsite_design_distances < threshold).float().mean().item()
|
| 842 |
+
)
|
| 843 |
+
|
| 844 |
+
# Count free Cysteines
|
| 845 |
+
if self.free_cys:
|
| 846 |
+
cysteine_mask = (
|
| 847 |
+
torch.argmax(feat["res_type"], dim=-1)
|
| 848 |
+
== const.token_ids["CYS"] & feat["design_mask"]
|
| 849 |
+
)
|
| 850 |
+
cysteine_sulfur_indices = (
|
| 851 |
+
torch.argmax(feat["token_to_rep_atom"].int(), dim=-1)[cysteine_mask]
|
| 852 |
+
+ const.ref_atoms["CYS"].index("SG")
|
| 853 |
+
- const.ref_atoms["CYS"].index("CA")
|
| 854 |
+
)
|
| 855 |
+
cysteine_coords = feat["coords"][0][cysteine_sulfur_indices]
|
| 856 |
+
free_cysteines = 0
|
| 857 |
+
dist = torch.cdist(cysteine_coords, cysteine_coords)
|
| 858 |
+
for i in range(dist.shape[0]):
|
| 859 |
+
if dist[i, torch.argsort(dist[i])[1]] > 4.0:
|
| 860 |
+
free_cysteines += 1
|
| 861 |
+
metrics["free_cysteines"] = free_cysteines
|
| 862 |
+
|
| 863 |
+
# Quality of Cysteine-Cysteine bonds
|
| 864 |
+
if self.disulfide_quality:
|
| 865 |
+
bonds = feat["structure_bonds"]
|
| 866 |
+
sulfur_mask = (
|
| 867 |
+
torch.argmax(feat["ref_element"], dim=-1)
|
| 868 |
+
== const.element_to_atomic_num["S"]
|
| 869 |
+
)
|
| 870 |
+
dist = []
|
| 871 |
+
disulfide_bonds = []
|
| 872 |
+
for bond in bonds:
|
| 873 |
+
if (
|
| 874 |
+
bond[6] == const.bond_type_ids["COVALENT"]
|
| 875 |
+
and sulfur_mask[bond[4]]
|
| 876 |
+
and sulfur_mask[bond[5]]
|
| 877 |
+
):
|
| 878 |
+
disulfide_bonds.append(bond)
|
| 879 |
+
for ds_bond in disulfide_bonds:
|
| 880 |
+
dist.append(
|
| 881 |
+
torch.cdist(
|
| 882 |
+
feat["coords"][0][ds_bond[4]].unsqueeze(0),
|
| 883 |
+
feat["coords"][0][ds_bond[5]].unsqueeze(0),
|
| 884 |
+
)
|
| 885 |
+
)
|
| 886 |
+
if len(dist) > 0:
|
| 887 |
+
min_dist = torch.cat(dist).min()
|
| 888 |
+
max_dist = torch.cat(dist).max()
|
| 889 |
+
metrics["disulfide_bond_len_qual"] = [max_dist.item(), min_dist.item()]
|
| 890 |
+
|
| 891 |
+
# Folding metrics
|
| 892 |
+
ca_coords_refolded = None
|
| 893 |
+
metrics["native_rmsd_refolded"] = 0.0
|
| 894 |
+
metrics["native_rmsd_bb_refolded"] = 0.0
|
| 895 |
+
if self.fold_metrics:
|
| 896 |
+
# Compute refolding metrics when refolding the design only in absence of the target (the whole design chain and anything covalently attached is refolded).
|
| 897 |
+
if self.designfolding_metrics:
|
| 898 |
+
folded_path = (
|
| 899 |
+
self.design_dir / const.folding_design_dirname / f"{feat['id']}.npz"
|
| 900 |
+
)
|
| 901 |
+
if not folded_path.exists():
|
| 902 |
+
print(f"Folded path does not exist. Skipping: {folded_path}")
|
| 903 |
+
return None
|
| 904 |
+
|
| 905 |
+
folded = np.load(
|
| 906 |
+
self.design_dir / const.folding_design_dirname / f"{feat['id']}.npz"
|
| 907 |
+
)
|
| 908 |
+
feat_design = {
|
| 909 |
+
k: torch.from_numpy(folded[k]).squeeze(0)
|
| 910 |
+
for k in [
|
| 911 |
+
"input_coords",
|
| 912 |
+
"res_type",
|
| 913 |
+
"token_index",
|
| 914 |
+
"atom_resolved_mask",
|
| 915 |
+
"atom_to_token",
|
| 916 |
+
"mol_type",
|
| 917 |
+
"backbone_mask",
|
| 918 |
+
]
|
| 919 |
+
}
|
| 920 |
+
feat_design["design_mask"] = torch.ones_like(
|
| 921 |
+
feat_design["token_index"]
|
| 922 |
+
).bool()
|
| 923 |
+
feat_design["chain_design_mask"] = torch.ones_like(
|
| 924 |
+
feat_design["token_index"]
|
| 925 |
+
).bool()
|
| 926 |
+
# Use the same features as refolded, just need to change the coordinates back to the original designed coordinates
|
| 927 |
+
feat_design["coords"] = feat_design["input_coords"]
|
| 928 |
+
if (
|
| 929 |
+
not len(folded["res_type"].squeeze())
|
| 930 |
+
== len(feat_design["res_type"])
|
| 931 |
+
or not (
|
| 932 |
+
folded["res_type"].squeeze() == feat_design["res_type"]
|
| 933 |
+
).all()
|
| 934 |
+
):
|
| 935 |
+
msg = f"Skipping {path}. The sequences for which the refolding was run are not the same as the sequences in the design_dir. Maybe the designs in the design_dir were overwritten. Or maybe two processes are operating on the same design_dir."
|
| 936 |
+
print(msg)
|
| 937 |
+
return None
|
| 938 |
+
|
| 939 |
+
# Compute all-atom folding metrics when refolding only the designed part (this does not make sense when using inverse folded structures)
|
| 940 |
+
if self.allatom_fold_metrics:
|
| 941 |
+
fold_metrics = get_fold_metrics(
|
| 942 |
+
feat_design,
|
| 943 |
+
folded,
|
| 944 |
+
compute_lddts=self.compute_lddts,
|
| 945 |
+
)
|
| 946 |
+
fold_metrics = {
|
| 947 |
+
f"designfolding-{k}": v for k, v in fold_metrics.items()
|
| 948 |
+
}
|
| 949 |
+
metrics.update(fold_metrics)
|
| 950 |
+
|
| 951 |
+
# Compute backbone folding metrics when refolding only the designed part
|
| 952 |
+
if self.backbone_fold_metrics:
|
| 953 |
+
feat_bb = copy.deepcopy(feat_design)
|
| 954 |
+
feat_bb["atom_resolved_mask"] = feat_bb["atom_resolved_mask"].to(
|
| 955 |
+
bool
|
| 956 |
+
) & feat_bb["backbone_mask"].to(bool)
|
| 957 |
+
fold_metrics_bb = get_fold_metrics(
|
| 958 |
+
feat_bb,
|
| 959 |
+
folded,
|
| 960 |
+
compute_lddts=self.compute_lddts,
|
| 961 |
+
prefix="bb_",
|
| 962 |
+
)
|
| 963 |
+
fold_metrics_bb = {
|
| 964 |
+
f"designfolding-{k}": v for k, v in fold_metrics_bb.items()
|
| 965 |
+
}
|
| 966 |
+
metrics.update(fold_metrics_bb)
|
| 967 |
+
|
| 968 |
+
folded_path = self.design_dir / const.folding_dirname / f"{feat['id']}.npz"
|
| 969 |
+
if not folded_path.exists():
|
| 970 |
+
print(f"Folded path does not exist. Skipping: {folded_path}")
|
| 971 |
+
return None
|
| 972 |
+
folded = np.load(
|
| 973 |
+
self.design_dir / const.folding_dirname / f"{feat['id']}.npz"
|
| 974 |
+
)
|
| 975 |
+
if (
|
| 976 |
+
not len(folded["res_type"].squeeze()) == len(feat["res_type"])
|
| 977 |
+
or not (folded["res_type"].squeeze() == feat["res_type"].numpy()).all()
|
| 978 |
+
):
|
| 979 |
+
msg = f"Skipping {path}. The sequences for which the refolding was run are not the same as the sequences in the design_dir. Maybe the designs in the design_dir were overwritten. Or maybe two processes are operating on the same design_dir."
|
| 980 |
+
print(msg)
|
| 981 |
+
return None
|
| 982 |
+
|
| 983 |
+
# Compute allatom folding metrics when refolding the whole complex (this does not make sense when the design_dir contains inverse folded structures).
|
| 984 |
+
if self.allatom_fold_metrics:
|
| 985 |
+
fold_metrics = get_fold_metrics(
|
| 986 |
+
feat,
|
| 987 |
+
folded,
|
| 988 |
+
compute_lddts=self.compute_lddts,
|
| 989 |
+
)
|
| 990 |
+
metrics.update(fold_metrics)
|
| 991 |
+
|
| 992 |
+
# Compute backbone folding metrics when refolding the whole complex
|
| 993 |
+
if self.backbone_fold_metrics:
|
| 994 |
+
feat_bb = copy.deepcopy(feat)
|
| 995 |
+
feat_bb["atom_resolved_mask"] = feat_bb["atom_resolved_mask"].to(
|
| 996 |
+
bool
|
| 997 |
+
) & feat_bb["backbone_mask"].to(bool)
|
| 998 |
+
fold_metrics_bb = get_fold_metrics(
|
| 999 |
+
feat_bb, folded, compute_lddts=self.compute_lddts, prefix="bb_"
|
| 1000 |
+
)
|
| 1001 |
+
fold_metrics_bb = {f"{k}": v for k, v in fold_metrics_bb.items()}
|
| 1002 |
+
metrics.update(fold_metrics_bb)
|
| 1003 |
+
|
| 1004 |
+
# Construct features for refolded complex
|
| 1005 |
+
feat_out = {}
|
| 1006 |
+
for k in feat.keys():
|
| 1007 |
+
if k == "coords":
|
| 1008 |
+
best_sample = get_best_folding_sample(folded)
|
| 1009 |
+
feat_out[k] = torch.from_numpy(best_sample["coords"])
|
| 1010 |
+
else:
|
| 1011 |
+
feat_out[k] = feat[k]
|
| 1012 |
+
refold_atom_target_resolved_mask = (
|
| 1013 |
+
(
|
| 1014 |
+
feat_out["atom_to_token"].float()
|
| 1015 |
+
@ target_resolved_mask.unsqueeze(-1).float()
|
| 1016 |
+
)
|
| 1017 |
+
.bool()
|
| 1018 |
+
.squeeze()
|
| 1019 |
+
)
|
| 1020 |
+
refold_target_coords = feat_out["coords"][
|
| 1021 |
+
refold_atom_target_resolved_mask, :
|
| 1022 |
+
][None, ...]
|
| 1023 |
+
|
| 1024 |
+
# Compute reconstruction RMDS compared to a native binder structure (if a native binder exists).
|
| 1025 |
+
if self.native:
|
| 1026 |
+
refold_target_rmsd = compute_rmsd(
|
| 1027 |
+
native_target_coords,
|
| 1028 |
+
refold_target_coords,
|
| 1029 |
+
)
|
| 1030 |
+
bb_refold_target_coords = feat_out["coords"][
|
| 1031 |
+
refold_atom_target_resolved_mask & feat_out["backbone_mask"].bool()
|
| 1032 |
+
][None, ...]
|
| 1033 |
+
bb_refold_target_rmsd = compute_rmsd(
|
| 1034 |
+
bb_native_target_coords,
|
| 1035 |
+
bb_refold_target_coords,
|
| 1036 |
+
)
|
| 1037 |
+
metrics["native_rmsd_refolded"] = refold_target_rmsd.item()
|
| 1038 |
+
metrics["native_rmsd_bb_refolded"] = bb_refold_target_rmsd.item()
|
| 1039 |
+
|
| 1040 |
+
# Save the refolded structure of the design to a pdb file if novelty computation needs to be run on it later.
|
| 1041 |
+
des_refold_pdb_path = (
|
| 1042 |
+
self.des_refold_pdb_dir / f"{feat['id']}_des_refold.pdb"
|
| 1043 |
+
)
|
| 1044 |
+
des_refold_cif_path = des_refold_pdb_path.with_suffix(".cif")
|
| 1045 |
+
if self.novelty_refolded or self.novelty_per_target_refolded:
|
| 1046 |
+
structure, _, _ = Structure.from_feat(feat_out)
|
| 1047 |
+
try:
|
| 1048 |
+
save_design_only_structure_to_pdb(
|
| 1049 |
+
atom_design_mask=atom_chain_mask,
|
| 1050 |
+
structure=structure,
|
| 1051 |
+
output_path=des_refold_pdb_path,
|
| 1052 |
+
)
|
| 1053 |
+
except Exception as e:
|
| 1054 |
+
print(
|
| 1055 |
+
f"[Warning] Could not save design-only structure for {feat['id']}: {e}. Skipping this file."
|
| 1056 |
+
)
|
| 1057 |
+
traceback.print_exc()
|
| 1058 |
+
return None
|
| 1059 |
+
|
| 1060 |
+
# largest hydrophobic patch area refolded
|
| 1061 |
+
if self.largest_hydrophobic_refolded:
|
| 1062 |
+
if not des_refold_cif_path.exists():
|
| 1063 |
+
structure, _, _ = Structure.from_feat(feat_out)
|
| 1064 |
+
|
| 1065 |
+
save_design_only_structure_to_cif(
|
| 1066 |
+
atom_design_mask=atom_chain_mask,
|
| 1067 |
+
structure=structure,
|
| 1068 |
+
output_path=des_refold_cif_path,
|
| 1069 |
+
)
|
| 1070 |
+
area_refold = largest_hydrophobic_patch_area(des_refold_cif_path)
|
| 1071 |
+
metrics["design_largest_hydrophobic_patch_refolded"] = area_refold
|
| 1072 |
+
if des_refold_cif_path is not None:
|
| 1073 |
+
des_refold_cif_path.unlink(missing_ok=True)
|
| 1074 |
+
|
| 1075 |
+
# Compute sequence based hydrophobicity
|
| 1076 |
+
metrics["design_chain_hydrophobicity"] = calc_hydrophobicity(
|
| 1077 |
+
design_chain_seq
|
| 1078 |
+
)
|
| 1079 |
+
metrics["design_hydrophobicity"] = calc_hydrophobicity(design_seq)
|
| 1080 |
+
|
| 1081 |
+
# delta sasa for refolded
|
| 1082 |
+
if self.delta_sasa_refolded:
|
| 1083 |
+
cif_path_refolded = self.refold_cif_dir / f"{feat['id']}.cif"
|
| 1084 |
+
|
| 1085 |
+
if not cif_path_refolded.exists():
|
| 1086 |
+
msg = f"Refolded cif path does not exist. This can happen if a process was interrupted between writing the refold .npz file and the refold .cif file. Missing path: {cif_path_refolded}"
|
| 1087 |
+
print(msg)
|
| 1088 |
+
return None
|
| 1089 |
+
|
| 1090 |
+
# Compute delta sasa
|
| 1091 |
+
(
|
| 1092 |
+
delta_sasa_refolded,
|
| 1093 |
+
design_sasa_unbound,
|
| 1094 |
+
design_sasa_bound,
|
| 1095 |
+
) = get_delta_sasa(
|
| 1096 |
+
cif_path_refolded,
|
| 1097 |
+
atom_target_mask=resolved_atoms_target_mask,
|
| 1098 |
+
atom_design_mask=resolved_atoms_design_mask,
|
| 1099 |
+
)
|
| 1100 |
+
|
| 1101 |
+
metrics["delta_sasa_refolded"] = delta_sasa_refolded
|
| 1102 |
+
metrics["design_sasa_unbound_refolded"] = design_sasa_unbound
|
| 1103 |
+
metrics["design_sasa_bound_refolded"] = design_sasa_bound
|
| 1104 |
+
|
| 1105 |
+
# noncovalents metrics for refolded structure
|
| 1106 |
+
if self.noncovalents_refolded:
|
| 1107 |
+
try:
|
| 1108 |
+
_metrics = count_noncovalents(feat_out)
|
| 1109 |
+
_metrics = {f"{k}_refolded": v for k, v in _metrics.items()}
|
| 1110 |
+
metrics.update(_metrics)
|
| 1111 |
+
except Exception as e:
|
| 1112 |
+
print(
|
| 1113 |
+
f"[Error] computing noncovalents refolded for {path}: {e}. Skipping this file."
|
| 1114 |
+
)
|
| 1115 |
+
traceback.print_exc()
|
| 1116 |
+
return None
|
| 1117 |
+
|
| 1118 |
+
bb_out = feat_out["coords"][bb_design_mask].reshape(-1, 4, 3)
|
| 1119 |
+
ca_coords_refolded = bb_out[:, 1, :].cpu()
|
| 1120 |
+
|
| 1121 |
+
# Affinity metrics
|
| 1122 |
+
if self.affinity_metrics:
|
| 1123 |
+
affinity_path = (
|
| 1124 |
+
self.design_dir / const.affinity_dirname / f"{feat['id']}.npz"
|
| 1125 |
+
)
|
| 1126 |
+
if not affinity_path.exists():
|
| 1127 |
+
print(f"Affinity path does not exist. Skipping: {affinity_path}")
|
| 1128 |
+
return None
|
| 1129 |
+
|
| 1130 |
+
affinity = np.load(
|
| 1131 |
+
self.design_dir / const.affinity_dirname / f"{feat['id']}.npz"
|
| 1132 |
+
)
|
| 1133 |
+
|
| 1134 |
+
for key in const.eval_keys_affinity:
|
| 1135 |
+
if key in affinity:
|
| 1136 |
+
metrics[key] = affinity[key].item()
|
| 1137 |
+
|
| 1138 |
+
if "affinity_probability_binary1" in metrics:
|
| 1139 |
+
metrics["affinity_probability_binary1>50"] = (
|
| 1140 |
+
metrics["affinity_probability_binary1"] > 0.5
|
| 1141 |
+
)
|
| 1142 |
+
metrics["affinity_probability_binary1>75"] = (
|
| 1143 |
+
metrics["affinity_probability_binary1"] > 0.75
|
| 1144 |
+
)
|
| 1145 |
+
|
| 1146 |
+
for key in const.eval_keys_confidence:
|
| 1147 |
+
if key in feat:
|
| 1148 |
+
if isinstance(feat[key], torch.Tensor) and feat[key].numel() == 1:
|
| 1149 |
+
metrics[key] = feat[key].item()
|
| 1150 |
+
elif isinstance(feat[key], (float, int)):
|
| 1151 |
+
metrics[key] = feat[key]
|
| 1152 |
+
|
| 1153 |
+
# Write outputs to files and return sample_id for conformation of successful processing
|
| 1154 |
+
data = {
|
| 1155 |
+
"target_id": target_id,
|
| 1156 |
+
"sample_id": sample_id,
|
| 1157 |
+
"design_seq": design_seq_tensor.cpu(),
|
| 1158 |
+
"ca_coords": ca_coords.cpu(),
|
| 1159 |
+
"ca_coords_refolded": ca_coords_refolded,
|
| 1160 |
+
}
|
| 1161 |
+
data_path = self.metrics_dir / f"data_{sample_id}.npz"
|
| 1162 |
+
metrics_path = self.metrics_dir / f"metrics_{sample_id}.npz"
|
| 1163 |
+
np.savez_compressed(metrics_path, **metrics)
|
| 1164 |
+
np.savez_compressed(data_path, **data)
|
| 1165 |
+
return sample_id
|
| 1166 |
+
|
| 1167 |
+
def compute_diversity(self, all_data, all_metrics):
|
| 1168 |
+
avg_metrics = {}
|
| 1169 |
+
metrics_data = {}
|
| 1170 |
+
fold_metrics = self.fold_metrics
|
| 1171 |
+
|
| 1172 |
+
# Aggregate alpha carbon positions for diversity eval
|
| 1173 |
+
ca_gen = defaultdict(list)
|
| 1174 |
+
input_metrics = defaultdict(list)
|
| 1175 |
+
ca_refold = defaultdict(list)
|
| 1176 |
+
sequences = defaultdict(list)
|
| 1177 |
+
for i, data in enumerate(all_data):
|
| 1178 |
+
ca_gen[data["target_id"]].append(data["ca_coords"])
|
| 1179 |
+
ca_refold[data["target_id"]].append(data["ca_coords_refolded"])
|
| 1180 |
+
input_metrics[data["target_id"]].append(all_metrics[i])
|
| 1181 |
+
|
| 1182 |
+
seq = data["design_seq"]
|
| 1183 |
+
try:
|
| 1184 |
+
seq = "".join(
|
| 1185 |
+
[const.prot_token_to_letter[const.tokens[t]] for t in seq]
|
| 1186 |
+
)
|
| 1187 |
+
sequences[data["target_id"]].append(seq)
|
| 1188 |
+
except KeyError as e:
|
| 1189 |
+
print(
|
| 1190 |
+
f"[Error] KeyError '{e.args[0]}' for target_id: {data['target_id']}, sample_id: {data['sample_id']}"
|
| 1191 |
+
)
|
| 1192 |
+
print(
|
| 1193 |
+
f"Number of targets: {len(ca_gen)}. Number of designs: {len(all_metrics)}."
|
| 1194 |
+
)
|
| 1195 |
+
|
| 1196 |
+
if self.diversity_original:
|
| 1197 |
+
print("Computing diveristy original.")
|
| 1198 |
+
ca_filtered = [ca[0] for ca in ca_gen.values() if len(ca[0]) >= 3]
|
| 1199 |
+
metrics_filtered = [
|
| 1200 |
+
m[0]
|
| 1201 |
+
for ca, m in zip(ca_gen.values(), input_metrics.values())
|
| 1202 |
+
if len(ca[0]) >= 3
|
| 1203 |
+
]
|
| 1204 |
+
scores = vendi_scores(
|
| 1205 |
+
all_ca_coords=ca_filtered,
|
| 1206 |
+
all_metrics=metrics_filtered,
|
| 1207 |
+
fold_metrics=fold_metrics,
|
| 1208 |
+
diversity_subset=self.diversity_subset,
|
| 1209 |
+
compute_lddts=self.compute_lddts,
|
| 1210 |
+
backbone_fold_metrics=self.backbone_fold_metrics,
|
| 1211 |
+
allatom_fold_metrics=self.allatom_fold_metrics,
|
| 1212 |
+
)
|
| 1213 |
+
for k, v in scores.items():
|
| 1214 |
+
avg_metrics[k + "_original"] = round(float(v), 5)
|
| 1215 |
+
|
| 1216 |
+
# Sequence diversity:
|
| 1217 |
+
seqs_filtered = [seq[0] for seq in sequences.values()]
|
| 1218 |
+
seq_scores = vendi_sequences(seqs_filtered, self.diversity_subset)
|
| 1219 |
+
for k, v in seq_scores.items():
|
| 1220 |
+
avg_metrics[k] = round(float(v), 5)
|
| 1221 |
+
|
| 1222 |
+
if self.diversity_per_target_original:
|
| 1223 |
+
print("Computing diveristy original per target.")
|
| 1224 |
+
vendi_per_target = []
|
| 1225 |
+
for target_id, ca_list in ca_gen.items():
|
| 1226 |
+
seq_list = sequences[target_id]
|
| 1227 |
+
ca_filtered = [e for e in ca_list if len(e) >= 3]
|
| 1228 |
+
metrics_filtered = [
|
| 1229 |
+
m
|
| 1230 |
+
for ca, m in zip(ca_list, input_metrics[target_id])
|
| 1231 |
+
if len(ca[0]) >= 3
|
| 1232 |
+
]
|
| 1233 |
+
count = len(ca_filtered)
|
| 1234 |
+
scores = vendi_scores(
|
| 1235 |
+
ca_filtered,
|
| 1236 |
+
all_metrics=metrics_filtered,
|
| 1237 |
+
fold_metrics=fold_metrics,
|
| 1238 |
+
diversity_subset=self.diversity_subset,
|
| 1239 |
+
compute_lddts=self.compute_lddts,
|
| 1240 |
+
backbone_fold_metrics=self.backbone_fold_metrics,
|
| 1241 |
+
allatom_fold_metrics=self.allatom_fold_metrics,
|
| 1242 |
+
)
|
| 1243 |
+
seq_scores = vendi_sequences(seq_list, self.diversity_subset)
|
| 1244 |
+
scores.update(seq_scores)
|
| 1245 |
+
scores.update(
|
| 1246 |
+
{
|
| 1247 |
+
"target_id": target_id,
|
| 1248 |
+
"num_filtered_ca": count,
|
| 1249 |
+
}
|
| 1250 |
+
)
|
| 1251 |
+
vendi_per_target.append(scores)
|
| 1252 |
+
df_vendi = pd.DataFrame(vendi_per_target)
|
| 1253 |
+
vendi_csv_path = Path(self.design_dir) / f"vendi_per_target_{self.name}.csv"
|
| 1254 |
+
df_vendi.to_csv(vendi_csv_path, index=False, float_format="%.5f")
|
| 1255 |
+
|
| 1256 |
+
for k in vendi_per_target[0].keys():
|
| 1257 |
+
if isinstance(vendi_per_target[0][k], numbers.Number):
|
| 1258 |
+
vendis = [e[k] for e in vendi_per_target if not np.isnan(e[k])]
|
| 1259 |
+
avg_metrics[k + "_mean_per_target"] = float(np.mean(vendis))
|
| 1260 |
+
avg_metrics[k + "_median_per_target"] = float(np.median(vendis))
|
| 1261 |
+
|
| 1262 |
+
metrics_data[k] = vendis
|
| 1263 |
+
|
| 1264 |
+
if self.diversity_refolded and (self.fold_metrics):
|
| 1265 |
+
print("Computing diveristy refolded.")
|
| 1266 |
+
sample0 = [ca[0] for ca in ca_refold.values() if ca[0].shape[0] >= 3]
|
| 1267 |
+
metrics_filtered = [
|
| 1268 |
+
m[0]
|
| 1269 |
+
for ca, m in zip(ca_refold.values(), input_metrics.values())
|
| 1270 |
+
if len(ca[0]) >= 3
|
| 1271 |
+
]
|
| 1272 |
+
scores = vendi_scores(
|
| 1273 |
+
sample0,
|
| 1274 |
+
metrics_filtered,
|
| 1275 |
+
fold_metrics,
|
| 1276 |
+
self.diversity_subset,
|
| 1277 |
+
self.compute_lddts,
|
| 1278 |
+
backbone_fold_metrics=self.backbone_fold_metrics,
|
| 1279 |
+
allatom_fold_metrics=self.allatom_fold_metrics,
|
| 1280 |
+
)
|
| 1281 |
+
|
| 1282 |
+
for k, v in scores.items():
|
| 1283 |
+
avg_metrics[k + "_refolded"] = round(float(v), 5)
|
| 1284 |
+
|
| 1285 |
+
return avg_metrics, metrics_data
|
| 1286 |
+
|
| 1287 |
+
def compute_novelty(self, suffix=None):
|
| 1288 |
+
"""
|
| 1289 |
+
Novelty computation using foldseek.
|
| 1290 |
+
This function can be optionally run for computing novelty compared to a reference database which needs to be provided
|
| 1291 |
+
The compute_metrics function writes pdb files which are then used by this function.
|
| 1292 |
+
"""
|
| 1293 |
+
avg_metrics = {}
|
| 1294 |
+
metrics_data = {}
|
| 1295 |
+
|
| 1296 |
+
des_pdb_dir = Path(self.des_pdb_dir)
|
| 1297 |
+
design_dir = Path(self.design_dir)
|
| 1298 |
+
des_refold_pdb_dir = Path(self.des_refold_pdb_dir)
|
| 1299 |
+
if not suffix is None:
|
| 1300 |
+
des_pdb_dir = des_pdb_dir / suffix
|
| 1301 |
+
design_dir = design_dir / suffix
|
| 1302 |
+
des_refold_pdb_dir = des_refold_pdb_dir / suffix
|
| 1303 |
+
design_dir.mkdir(exist_ok=True, parents=True)
|
| 1304 |
+
|
| 1305 |
+
# novelty original
|
| 1306 |
+
if self.novelty_original or self.novelty_per_target_original:
|
| 1307 |
+
print("Computing novelty original.")
|
| 1308 |
+
novelty_original_df = compute_novelty_foldseek(
|
| 1309 |
+
indir=des_pdb_dir,
|
| 1310 |
+
outdir=design_dir,
|
| 1311 |
+
reference_db=self.foldseek_db,
|
| 1312 |
+
files=[str(p) for p in des_pdb_dir.glob("*.pdb")],
|
| 1313 |
+
foldseek_binary=self.foldseek_binary,
|
| 1314 |
+
)
|
| 1315 |
+
|
| 1316 |
+
if self.novelty_original:
|
| 1317 |
+
avg_metrics["novelty_original"] = round(
|
| 1318 |
+
float(novelty_original_df["novelty"].mean()), 5
|
| 1319 |
+
)
|
| 1320 |
+
|
| 1321 |
+
if self.novelty_per_target_original:
|
| 1322 |
+
novelty_original_df["target_id"] = novelty_original_df["query"].apply(
|
| 1323 |
+
lambda s: TARGET_ID_RE.match(s).group(1)
|
| 1324 |
+
)
|
| 1325 |
+
nov_df = (
|
| 1326 |
+
novelty_original_df.groupby("target_id")["novelty"].mean().reset_index()
|
| 1327 |
+
)
|
| 1328 |
+
nov_csv = Path(design_dir) / f"novelty_per_target_original_{self.name}.csv"
|
| 1329 |
+
nov_df.to_csv(nov_csv, index=False, float_format="%.5f")
|
| 1330 |
+
avg_metrics["mean_novelty_per_target_original"] = (
|
| 1331 |
+
nov_df["novelty"].mean().round(5)
|
| 1332 |
+
)
|
| 1333 |
+
avg_metrics["median_novelty_per_target_original"] = (
|
| 1334 |
+
nov_df["novelty"].median().round(5)
|
| 1335 |
+
)
|
| 1336 |
+
metrics_data["nov_df"] = nov_df
|
| 1337 |
+
|
| 1338 |
+
# Novelty refolded
|
| 1339 |
+
if (self.novelty_refolded or self.novelty_per_target_refolded) and (
|
| 1340 |
+
self.fold_metrics
|
| 1341 |
+
):
|
| 1342 |
+
print("Computing novelty refolded.")
|
| 1343 |
+
novelty_refolded_df = compute_novelty_foldseek(
|
| 1344 |
+
indir=des_refold_pdb_dir,
|
| 1345 |
+
outdir=Path(design_dir),
|
| 1346 |
+
reference_db=self.foldseek_db,
|
| 1347 |
+
files=[str(p) for p in des_refold_pdb_dir.glob("*.pdb")],
|
| 1348 |
+
foldseek_binary=self.foldseek_binary,
|
| 1349 |
+
)
|
| 1350 |
+
|
| 1351 |
+
if self.novelty_refolded and (self.fold_metrics):
|
| 1352 |
+
avg_metrics["novelty_refolded"] = round(
|
| 1353 |
+
float(novelty_refolded_df["novelty"].mean()), 5
|
| 1354 |
+
)
|
| 1355 |
+
|
| 1356 |
+
if self.novelty_per_target_refolded and (self.fold_metrics):
|
| 1357 |
+
novelty_refolded_df["target_id"] = novelty_refolded_df["query"].apply(
|
| 1358 |
+
lambda s: TARGET_ID_RE.match(s).group(1)
|
| 1359 |
+
)
|
| 1360 |
+
nov_df_refold = (
|
| 1361 |
+
novelty_refolded_df.groupby("target_id")["novelty"].mean().reset_index()
|
| 1362 |
+
)
|
| 1363 |
+
nov_csv = Path(design_dir) / f"novelty_per_target_refolded_{self.name}.csv"
|
| 1364 |
+
nov_df_refold.to_csv(nov_csv, index=False, float_format="%.5f")
|
| 1365 |
+
avg_metrics["mean_novelty_per_target_refolded"] = (
|
| 1366 |
+
nov_df_refold["novelty"].mean().round(5)
|
| 1367 |
+
)
|
| 1368 |
+
avg_metrics["median_novelty_per_target_refolded"] = round(
|
| 1369 |
+
nov_df_refold["novelty"].median(), 5
|
| 1370 |
+
)
|
| 1371 |
+
metrics_data["nov_df_refold"] = nov_df_refold
|
| 1372 |
+
return avg_metrics, metrics_data
|
| 1373 |
+
|
| 1374 |
+
def run_foldseek_clustering(self, df: pd.DataFrame, suffix=None) -> pd.DataFrame:
|
| 1375 |
+
"""
|
| 1376 |
+
Annotates each design with a cluster based on foldseek clustering.
|
| 1377 |
+
This function can be optionally run.
|
| 1378 |
+
The compute_metrics function writes pdb files which are then used by this function.
|
| 1379 |
+
"""
|
| 1380 |
+
des_pdb_dir = self.des_pdb_dir / suffix if suffix else self.des_pdb_dir
|
| 1381 |
+
design_dir = Path(self.design_dir) / suffix if suffix else Path(self.design_dir)
|
| 1382 |
+
|
| 1383 |
+
cluster_output_dir = design_dir / "foldseek_cluster"
|
| 1384 |
+
cluster_output_dir.mkdir(parents=True, exist_ok=True)
|
| 1385 |
+
|
| 1386 |
+
cluster_prefix = cluster_output_dir / "cluster"
|
| 1387 |
+
tmp_dir = cluster_output_dir / "tmp"
|
| 1388 |
+
|
| 1389 |
+
min_num_design = int(df["num_design"].min())
|
| 1390 |
+
cmd = [
|
| 1391 |
+
self.foldseek_binary,
|
| 1392 |
+
"easy-cluster",
|
| 1393 |
+
str(des_pdb_dir),
|
| 1394 |
+
str(cluster_prefix),
|
| 1395 |
+
str(tmp_dir),
|
| 1396 |
+
"--alignment-type",
|
| 1397 |
+
"1",
|
| 1398 |
+
"--cov-mode",
|
| 1399 |
+
"0",
|
| 1400 |
+
"--min-seq-id",
|
| 1401 |
+
"0",
|
| 1402 |
+
"--tmscore-threshold",
|
| 1403 |
+
"0.5",
|
| 1404 |
+
]
|
| 1405 |
+
if min_num_design < 20:
|
| 1406 |
+
msg = f"[FoldSeek] Using --kmer-per-seq {2} due to short designs."
|
| 1407 |
+
print(msg)
|
| 1408 |
+
cmd += ["--kmer-per-seq", str(2)]
|
| 1409 |
+
|
| 1410 |
+
try:
|
| 1411 |
+
subprocess.run(cmd, check=True)
|
| 1412 |
+
|
| 1413 |
+
df_cluster = pd.read_csv(
|
| 1414 |
+
str(cluster_prefix) + "_cluster.tsv",
|
| 1415 |
+
sep="\t",
|
| 1416 |
+
header=None,
|
| 1417 |
+
names=["file", "clusters_05_tmscore"],
|
| 1418 |
+
)
|
| 1419 |
+
df_cluster["file"] = df_cluster["file"].apply(lambda x: Path(x).stem)
|
| 1420 |
+
|
| 1421 |
+
df = df.merge(df_cluster, left_on="id", right_on="file", how="left").drop(
|
| 1422 |
+
columns=["file"]
|
| 1423 |
+
)
|
| 1424 |
+
except Exception as e:
|
| 1425 |
+
msg = f"Structure clustering was unsuccessful. No cluster labels are added to the dataframe / csv file output."
|
| 1426 |
+
print(msg)
|
| 1427 |
+
return df
|
| 1428 |
+
|
| 1429 |
+
def make_histograms(self, all_metrics):
|
| 1430 |
+
df = pd.DataFrame(all_metrics)
|
| 1431 |
+
|
| 1432 |
+
# Make aggregate histograms
|
| 1433 |
+
histograms = {}
|
| 1434 |
+
cols = [
|
| 1435 |
+
"delta_sasa_refolded",
|
| 1436 |
+
"rmsd",
|
| 1437 |
+
"iptm",
|
| 1438 |
+
"ptm",
|
| 1439 |
+
"rmsd",
|
| 1440 |
+
"design_ptm",
|
| 1441 |
+
"min_design_to_target_pae",
|
| 1442 |
+
"helix",
|
| 1443 |
+
"sheet",
|
| 1444 |
+
"loop",
|
| 1445 |
+
"plip_saltbridge",
|
| 1446 |
+
"plip_hbonds",
|
| 1447 |
+
"design_sasa_bound_original",
|
| 1448 |
+
"design_sasa_unbound_original",
|
| 1449 |
+
"delta_sasa_original",
|
| 1450 |
+
"num_design",
|
| 1451 |
+
"precision_loop",
|
| 1452 |
+
"recall_loop",
|
| 1453 |
+
"precision_helix",
|
| 1454 |
+
"recall_helix",
|
| 1455 |
+
"precision_sheet",
|
| 1456 |
+
"recall_sheet",
|
| 1457 |
+
"accuracy_overall",
|
| 1458 |
+
"liability_score",
|
| 1459 |
+
"liability_num_violations",
|
| 1460 |
+
]
|
| 1461 |
+
for col in cols:
|
| 1462 |
+
if col in df.columns:
|
| 1463 |
+
histograms["hist" + col] = make_histogram(df, col)
|
| 1464 |
+
|
| 1465 |
+
# make per target histograms
|
| 1466 |
+
df["target_id"] = df["id"].apply(lambda s: TARGET_ID_RE.match(s).group(1))
|
| 1467 |
+
per_target_df = df.groupby("target_id").mean(numeric_only=True).reset_index()
|
| 1468 |
+
cols += ["rmsd<2.5", "designability_rmsd_2"]
|
| 1469 |
+
if self.compute_lddts:
|
| 1470 |
+
cols += [
|
| 1471 |
+
"designability_lddt_90",
|
| 1472 |
+
"designability_lddt_85",
|
| 1473 |
+
"designability_lddt_80",
|
| 1474 |
+
"designability_lddt_75",
|
| 1475 |
+
"designability_lddt_70",
|
| 1476 |
+
"designability_lddt_65",
|
| 1477 |
+
"designability_lddt_60",
|
| 1478 |
+
]
|
| 1479 |
+
for col in cols:
|
| 1480 |
+
if col in df.columns:
|
| 1481 |
+
# Per target histograms
|
| 1482 |
+
histograms["per_target" + col] = make_histogram(per_target_df, col)
|
| 1483 |
+
return df, histograms
|
model/boltzgen/task/analyze/analyze_utils.py
ADDED
|
@@ -0,0 +1,1260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import warnings
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import random
|
| 5 |
+
from typing import List
|
| 6 |
+
import subprocess
|
| 7 |
+
import re
|
| 8 |
+
import biotite
|
| 9 |
+
import hydride
|
| 10 |
+
from sklearn.cluster import DBSCAN
|
| 11 |
+
from Bio import PDB
|
| 12 |
+
from biotite import structure
|
| 13 |
+
from Bio.Seq import Seq
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from matplotlib import pyplot as plt
|
| 17 |
+
import matplotlib as mpl
|
| 18 |
+
|
| 19 |
+
mpl.rcParams["figure.max_open_warning"] = 100
|
| 20 |
+
|
| 21 |
+
from Bio import Align
|
| 22 |
+
|
| 23 |
+
from boltzgen.data.rmsd_computation import get_true_coordinates
|
| 24 |
+
from boltzgen.model.loss.diffusion import weighted_rigid_align
|
| 25 |
+
from boltzgen.task.predict.data_from_generated import collate
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import numpy as np
|
| 29 |
+
import pandas as pd
|
| 30 |
+
from tqdm import tqdm
|
| 31 |
+
|
| 32 |
+
from boltzgen.data import const
|
| 33 |
+
from boltzgen.data.data import Structure, biotite_array_from_feat
|
| 34 |
+
from boltzgen.data.write.mmcif import to_mmcif
|
| 35 |
+
from boltzgen.model.loss.validation import factored_lddt_loss, compute_subset_rmsd
|
| 36 |
+
|
| 37 |
+
from biotite.structure.sasa import sasa
|
| 38 |
+
from biotite.structure.info import vdw_radius_single, vdw_radius_protor
|
| 39 |
+
import biotite.structure.io.pdbx as pdbx
|
| 40 |
+
import biotite.structure.io.pdb as pdbio
|
| 41 |
+
|
| 42 |
+
TARGET_ID_RE = re.compile(
|
| 43 |
+
r"^(?:(?:sample\d+_|batch\d+_|rank\d+_)+)?([^_]+)(?:_[^_]+)*?(?:_(?:gen))*$"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _load_stack(path):
|
| 48 |
+
path = Path(path)
|
| 49 |
+
suffix = path.suffix.lower()
|
| 50 |
+
if suffix in {".cif", ".mmcif"}:
|
| 51 |
+
cif_file = pdbx.CIFFile.read(str(path))
|
| 52 |
+
stack = pdbx.get_structure(cif_file, use_author_fields=False)
|
| 53 |
+
elif suffix in {".pdb", ".ent"}:
|
| 54 |
+
pdb_file = pdbio.PDBFile.read(str(path))
|
| 55 |
+
stack = pdbio.get_structure(pdb_file, model=None)
|
| 56 |
+
else:
|
| 57 |
+
raise ValueError(f"Unsupported structure file extension: {suffix}")
|
| 58 |
+
|
| 59 |
+
return stack
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def compute_rmsd(atom_coords: torch.Tensor, pred_atom_coords: torch.Tensor):
|
| 63 |
+
rmsd, _ = compute_subset_rmsd(
|
| 64 |
+
atom_coords,
|
| 65 |
+
pred_atom_coords,
|
| 66 |
+
atom_mask=torch.ones_like(atom_coords[..., 0]),
|
| 67 |
+
align_weights=torch.ones_like(atom_coords[..., 0]),
|
| 68 |
+
subset_mask=torch.ones_like(atom_coords[..., 0]),
|
| 69 |
+
multiplicity=1,
|
| 70 |
+
)
|
| 71 |
+
return rmsd
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def make_histogram(
|
| 75 |
+
df,
|
| 76 |
+
column_name: str,
|
| 77 |
+
):
|
| 78 |
+
data = df[column_name].dropna()
|
| 79 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 80 |
+
|
| 81 |
+
ax.hist(data, bins=50, alpha=0.7, edgecolor="black", linewidth=0.5)
|
| 82 |
+
ax.axvline(data.mean(), color="red", linestyle="dashed", linewidth=1)
|
| 83 |
+
|
| 84 |
+
ax.set_title(
|
| 85 |
+
f"{column_name.replace('_', ' ').capitalize()} Distribution", fontsize=12
|
| 86 |
+
)
|
| 87 |
+
ax.set_xlabel(column_name.replace("_", " "), fontsize=10)
|
| 88 |
+
ax.set_ylabel("Count", fontsize=10)
|
| 89 |
+
|
| 90 |
+
ax.grid(True, axis="y", linestyle="--", linewidth=0.5, alpha=0.6)
|
| 91 |
+
ax.tick_params(axis="both", which="major", labelsize=8)
|
| 92 |
+
|
| 93 |
+
plt.tight_layout()
|
| 94 |
+
return fig
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def get_best_folding_sample(folded):
|
| 98 |
+
confidence = 0.8 * folded["design_to_target_iptm"] + 0.2 * folded["design_ptm"]
|
| 99 |
+
best_idx = np.argmax(confidence)
|
| 100 |
+
|
| 101 |
+
# TODO: remove the "if k in folded"
|
| 102 |
+
best_sample = {
|
| 103 |
+
k: folded[k][best_idx] for k in const.eval_keys_confidence if k in folded
|
| 104 |
+
}
|
| 105 |
+
best_sample["coords"] = folded["coords"][best_idx]
|
| 106 |
+
return best_sample
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def get_fold_metrics(
|
| 110 |
+
feat,
|
| 111 |
+
folded,
|
| 112 |
+
compute_lddts=True,
|
| 113 |
+
prefix="",
|
| 114 |
+
):
|
| 115 |
+
batch = collate([feat])
|
| 116 |
+
diffusion_samples = batch["coords"].shape[0]
|
| 117 |
+
best_sample = get_best_folding_sample(folded)
|
| 118 |
+
|
| 119 |
+
# Compute RMSDs
|
| 120 |
+
rmsd_out = get_true_coordinates(
|
| 121 |
+
batch=batch,
|
| 122 |
+
out={"sample_atom_coords": torch.from_numpy(best_sample["coords"])},
|
| 123 |
+
diffusion_samples=1,
|
| 124 |
+
symmetry_correction=False,
|
| 125 |
+
protein_lig_rmsd=True,
|
| 126 |
+
)
|
| 127 |
+
true_coords_resolved_mask = rmsd_out["true_coords_resolved_mask"]
|
| 128 |
+
|
| 129 |
+
# Add to metrics dictionary
|
| 130 |
+
metrics = {}
|
| 131 |
+
metrics["rmsd"] = rmsd_out.get("rmsd").item()
|
| 132 |
+
metrics["rmsd_design"] = rmsd_out.get("rmsd_design").item()
|
| 133 |
+
metrics["rmsd_target"] = rmsd_out.get("rmsd_target").item()
|
| 134 |
+
metrics["rmsd_design_target"] = rmsd_out.get("rmsd_design_target").item()
|
| 135 |
+
metrics["target_aligned_rmsd_design"] = rmsd_out.get(
|
| 136 |
+
"target_aligned_rmsd_design"
|
| 137 |
+
).item()
|
| 138 |
+
metrics["rmsd<2.5"] = bool(metrics["rmsd"] <= 2.5)
|
| 139 |
+
metrics["target_aligned<2.5"] = bool(metrics["target_aligned_rmsd_design"] <= 2.5)
|
| 140 |
+
metrics["designability_rmsd_2"] = bool(metrics["rmsd_design"] <= 2.0)
|
| 141 |
+
metrics["designability_rmsd_4"] = bool(metrics["rmsd_design"] <= 4.0)
|
| 142 |
+
|
| 143 |
+
# Comput LDDTs
|
| 144 |
+
if compute_lddts:
|
| 145 |
+
all_lddt_dict, _ = factored_lddt_loss(
|
| 146 |
+
feats=batch,
|
| 147 |
+
atom_mask=true_coords_resolved_mask,
|
| 148 |
+
true_atom_coords=batch["coords"],
|
| 149 |
+
pred_atom_coords=torch.from_numpy(best_sample["coords"]),
|
| 150 |
+
multiplicity=diffusion_samples,
|
| 151 |
+
exclude_ions=False,
|
| 152 |
+
)
|
| 153 |
+
metrics.update({f"lddt_{k}": v.max().item() for k, v in all_lddt_dict.items()})
|
| 154 |
+
metrics["designability_lddt_60"] = bool(metrics["lddt_intra_design"] >= 0.6)
|
| 155 |
+
metrics["designability_lddt_65"] = bool(metrics["lddt_intra_design"] >= 0.65)
|
| 156 |
+
metrics["designability_lddt_70"] = bool(metrics["lddt_intra_design"] >= 0.7)
|
| 157 |
+
metrics["designability_lddt_75"] = bool(metrics["lddt_intra_design"] >= 0.75)
|
| 158 |
+
metrics["designability_lddt_80"] = bool(metrics["lddt_intra_design"] >= 0.8)
|
| 159 |
+
metrics["designability_lddt_85"] = bool(metrics["lddt_intra_design"] >= 0.85)
|
| 160 |
+
metrics["designability_lddt_90"] = bool(metrics["lddt_intra_design"] >= 0.9)
|
| 161 |
+
|
| 162 |
+
# metrics without prefix (backbone only is the same as all atom)
|
| 163 |
+
# TODO: remove the "if k in best_sample"
|
| 164 |
+
confs = {k: best_sample[k] for k in const.eval_keys_confidence if k in best_sample}
|
| 165 |
+
confs["min_interaction_pae<1.5"] = bool(confs["min_interaction_pae"] <= 1.5)
|
| 166 |
+
confs["min_interaction_pae<2"] = bool(confs["min_interaction_pae"] <= 2.0)
|
| 167 |
+
confs["min_interaction_pae<2.5"] = bool(confs["min_interaction_pae"] <= 2.5)
|
| 168 |
+
confs["min_interaction_pae<3"] = bool(confs["min_interaction_pae"] <= 3)
|
| 169 |
+
confs["min_interaction_pae<4"] = bool(confs["min_interaction_pae"] <= 4)
|
| 170 |
+
confs["min_interaction_pae<5"] = bool(confs["min_interaction_pae"] <= 5)
|
| 171 |
+
confs["design_ptm>80"] = bool(confs["design_ptm"] >= 0.8)
|
| 172 |
+
confs["design_ptm>75"] = bool(confs["design_ptm"] >= 0.75)
|
| 173 |
+
confs["design_iptm>80"] = bool(confs["design_iptm"] >= 0.8)
|
| 174 |
+
confs["design_iptm>70"] = bool(confs["design_iptm"] >= 0.7)
|
| 175 |
+
confs["design_iptm>60"] = bool(confs["design_iptm"] >= 0.6)
|
| 176 |
+
confs["design_iptm>50"] = bool(confs["design_iptm"] >= 0.5)
|
| 177 |
+
|
| 178 |
+
prefixed_metrics = {f"{prefix}{k}": v for k, v in metrics.items()}
|
| 179 |
+
prefixed_metrics.update(confs)
|
| 180 |
+
return prefixed_metrics
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def count_noncovalents(feat):
|
| 184 |
+
metrics = {}
|
| 185 |
+
with warnings.catch_warnings():
|
| 186 |
+
warnings.simplefilter("ignore")
|
| 187 |
+
biotite_array = biotite_array_from_feat(feat)
|
| 188 |
+
biotite_array, _ = hydride.add_hydrogen(biotite_array)
|
| 189 |
+
hbond = biotite.structure.hbond(biotite_array)
|
| 190 |
+
donor_idxs, acceptor_idxs = hbond[:, 0], hbond[:, 2]
|
| 191 |
+
donor_design_hbonds = int(
|
| 192 |
+
(
|
| 193 |
+
biotite_array.is_design[donor_idxs]
|
| 194 |
+
& ~biotite_array.is_chain_design[acceptor_idxs]
|
| 195 |
+
).sum()
|
| 196 |
+
)
|
| 197 |
+
acceptor_design_hbonds = int(
|
| 198 |
+
(
|
| 199 |
+
~biotite_array.is_chain_design[donor_idxs]
|
| 200 |
+
& biotite_array.is_design[acceptor_idxs]
|
| 201 |
+
).sum()
|
| 202 |
+
)
|
| 203 |
+
metrics["plip_hbonds"] = donor_design_hbonds + acceptor_design_hbonds
|
| 204 |
+
|
| 205 |
+
# saltbridges
|
| 206 |
+
pos_atoms = biotite_array[biotite_array.charge > 0]
|
| 207 |
+
neg_atoms = biotite_array[biotite_array.charge < 0]
|
| 208 |
+
if len(neg_atoms) > 0 and len(pos_atoms) > 0:
|
| 209 |
+
pos_neg_distances = torch.cdist(
|
| 210 |
+
torch.as_tensor(pos_atoms.coord), torch.as_tensor(neg_atoms.coord)
|
| 211 |
+
)
|
| 212 |
+
pos_idxs, neg_idxs = torch.where(
|
| 213 |
+
(pos_neg_distances > 0.5) & (pos_neg_distances < 5.5)
|
| 214 |
+
)
|
| 215 |
+
# only keep the ones between design and non design
|
| 216 |
+
pos_design_sb = int(
|
| 217 |
+
(pos_atoms.is_design[pos_idxs] & ~neg_atoms.is_chain_design[neg_idxs]).sum()
|
| 218 |
+
)
|
| 219 |
+
neg_design_sb = int(
|
| 220 |
+
(~pos_atoms.is_chain_design[pos_idxs] & neg_atoms.is_design[neg_idxs]).sum()
|
| 221 |
+
)
|
| 222 |
+
metrics["plip_saltbridge"] = pos_design_sb + neg_design_sb
|
| 223 |
+
else:
|
| 224 |
+
metrics["plip_saltbridge"] = 0
|
| 225 |
+
return metrics
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def tm_score(coords1, coords2):
|
| 229 |
+
num_atoms1 = coords1.shape[0]
|
| 230 |
+
num_atoms2 = coords2.shape[0]
|
| 231 |
+
|
| 232 |
+
atom_array1 = structure.AtomArray(num_atoms1)
|
| 233 |
+
atom_array1.coord = coords1.numpy()
|
| 234 |
+
atom_array1.element = np.array(["C"] * num_atoms1)
|
| 235 |
+
atom_array1.atom_name = np.array(["CA"] * num_atoms1)
|
| 236 |
+
atom_array1.res_name = np.array(["ALA"] * num_atoms1)
|
| 237 |
+
atom_array1.chain_id = np.array(["A"] * num_atoms1)
|
| 238 |
+
atom_array1.res_id = np.arange(1, num_atoms1 + 1)
|
| 239 |
+
|
| 240 |
+
atom_array2 = structure.AtomArray(num_atoms2)
|
| 241 |
+
atom_array2.coord = coords2.numpy()
|
| 242 |
+
atom_array2.element = np.array(["C"] * num_atoms2)
|
| 243 |
+
atom_array2.atom_name = np.array(["CA"] * num_atoms2)
|
| 244 |
+
atom_array2.res_name = np.array(["ALA"] * num_atoms2)
|
| 245 |
+
atom_array2.chain_id = np.array(["A"] * num_atoms2)
|
| 246 |
+
atom_array2.res_id = np.arange(1, num_atoms2 + 1)
|
| 247 |
+
|
| 248 |
+
try:
|
| 249 |
+
# This fails with a value error if the structures are too dissimilar. In that event, we return 0 as the TM-Score
|
| 250 |
+
aligned, transform, fixed_indices, mobile_indices = (
|
| 251 |
+
structure.superimpose_structural_homologs(
|
| 252 |
+
atom_array1, atom_array2, max_iterations=25
|
| 253 |
+
)
|
| 254 |
+
)
|
| 255 |
+
tm_align_fixed = structure.tm_score(
|
| 256 |
+
atom_array1,
|
| 257 |
+
aligned,
|
| 258 |
+
fixed_indices,
|
| 259 |
+
mobile_indices,
|
| 260 |
+
)
|
| 261 |
+
except:
|
| 262 |
+
tm_align_fixed = 0
|
| 263 |
+
|
| 264 |
+
tm_score_rmsd_aligned = 0
|
| 265 |
+
if num_atoms1 == num_atoms2:
|
| 266 |
+
coords1 = weighted_rigid_align(
|
| 267 |
+
coords1.float()[None],
|
| 268 |
+
coords2.float()[None],
|
| 269 |
+
weights=torch.ones(len(coords1)).float()[None],
|
| 270 |
+
mask=torch.ones(len(coords2))[None],
|
| 271 |
+
).squeeze()
|
| 272 |
+
|
| 273 |
+
atom_array1 = structure.AtomArray(num_atoms1)
|
| 274 |
+
atom_array1.coord = coords1.numpy()
|
| 275 |
+
atom_array1.element = np.array(["C"] * num_atoms1)
|
| 276 |
+
atom_array1.atom_name = np.array(["CA"] * num_atoms1)
|
| 277 |
+
atom_array1.res_name = np.array(["ALA"] * num_atoms1)
|
| 278 |
+
atom_array1.chain_id = np.array(["A"] * num_atoms1)
|
| 279 |
+
atom_array1.res_id = np.arange(1, num_atoms1 + 1)
|
| 280 |
+
|
| 281 |
+
atom_array2 = structure.AtomArray(num_atoms2)
|
| 282 |
+
atom_array2.coord = coords2.numpy()
|
| 283 |
+
atom_array2.element = np.array(["C"] * num_atoms2)
|
| 284 |
+
atom_array2.atom_name = np.array(["CA"] * num_atoms2)
|
| 285 |
+
atom_array2.res_name = np.array(["ALA"] * num_atoms2)
|
| 286 |
+
atom_array2.chain_id = np.array(["A"] * num_atoms2)
|
| 287 |
+
atom_array2.res_id = np.arange(1, num_atoms2 + 1)
|
| 288 |
+
try:
|
| 289 |
+
_, _, fixed_indices, mobile_indices = (
|
| 290 |
+
structure.superimpose_structural_homologs(
|
| 291 |
+
atom_array1, atom_array2, max_iterations=25
|
| 292 |
+
)
|
| 293 |
+
)
|
| 294 |
+
tm_score_rmsd_aligned = structure.tm_score(
|
| 295 |
+
atom_array1,
|
| 296 |
+
atom_array2,
|
| 297 |
+
fixed_indices,
|
| 298 |
+
mobile_indices,
|
| 299 |
+
)
|
| 300 |
+
except:
|
| 301 |
+
pass
|
| 302 |
+
|
| 303 |
+
return tm_score_rmsd_aligned, tm_align_fixed
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def vendi_from_sim(mat):
|
| 307 |
+
mat = mat + mat.T
|
| 308 |
+
np.fill_diagonal(mat, 1.0)
|
| 309 |
+
eigvals, _ = np.linalg.eigh(mat / len(mat))
|
| 310 |
+
eigvals = np.clip(eigvals, 0.0, None)
|
| 311 |
+
return np.exp(np.nansum(-(eigvals * np.log(eigvals))))
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def vendi_scores(
|
| 315 |
+
all_ca_coords: List[np.ndarray],
|
| 316 |
+
all_metrics: list = None,
|
| 317 |
+
fold_metrics: bool = False,
|
| 318 |
+
diversity_subset: int = None,
|
| 319 |
+
compute_lddts: bool = True,
|
| 320 |
+
compute_iptms: bool = True,
|
| 321 |
+
compute_min_int_paes: bool = True,
|
| 322 |
+
backbone_fold_metrics: bool = False,
|
| 323 |
+
allatom_fold_metrics: bool = True,
|
| 324 |
+
) -> float:
|
| 325 |
+
if fold_metrics or diversity_subset is not None:
|
| 326 |
+
assert all_metrics is not None
|
| 327 |
+
if all_metrics is not None:
|
| 328 |
+
assert len(all_ca_coords) == len(all_metrics)
|
| 329 |
+
if diversity_subset is not None and diversity_subset < len(all_ca_coords):
|
| 330 |
+
indices = random.sample(range(len(all_ca_coords)), diversity_subset)
|
| 331 |
+
all_metrics = [all_metrics[i] for i in indices]
|
| 332 |
+
all_ca_coords = [all_ca_coords[i] for i in indices]
|
| 333 |
+
N = len(all_ca_coords)
|
| 334 |
+
tm = np.zeros((N, N), dtype=np.float32)
|
| 335 |
+
tm_fixed = np.zeros((N, N), dtype=np.float32)
|
| 336 |
+
|
| 337 |
+
for i in tqdm(range(N), desc="Computing structure diversity."):
|
| 338 |
+
for j in range(i + 1, N):
|
| 339 |
+
tm_score_rmsd_aligned, tm_fixeds = tm_score(
|
| 340 |
+
all_ca_coords[i], all_ca_coords[j]
|
| 341 |
+
)
|
| 342 |
+
tm[i, j] = tm_score_rmsd_aligned
|
| 343 |
+
tm_fixed[i, j] = tm_fixeds
|
| 344 |
+
|
| 345 |
+
scores = {
|
| 346 |
+
"vendi_tm_fixed": vendi_from_sim(tm_fixed),
|
| 347 |
+
"vendi_tm_align": vendi_from_sim(tm),
|
| 348 |
+
}
|
| 349 |
+
prefixes = []
|
| 350 |
+
if allatom_fold_metrics:
|
| 351 |
+
prefixes.append("")
|
| 352 |
+
if backbone_fold_metrics:
|
| 353 |
+
prefixes.append("bb_")
|
| 354 |
+
for prefix in prefixes:
|
| 355 |
+
mask_2 = np.array([m[f"{prefix}designability_rmsd_2"] for m in all_metrics])
|
| 356 |
+
mask_4 = np.array([m[f"{prefix}designability_rmsd_4"] for m in all_metrics])
|
| 357 |
+
mask_25 = np.array([m[f"{prefix}rmsd<2.5"] for m in all_metrics])
|
| 358 |
+
mask_target_25 = np.array(
|
| 359 |
+
[m[f"{prefix}target_aligned<2.5"] for m in all_metrics]
|
| 360 |
+
)
|
| 361 |
+
scores.update(
|
| 362 |
+
{
|
| 363 |
+
f"vendi_tm_{prefix}rmsd<2.5": vendi_from_sim(
|
| 364 |
+
tm_fixed[mask_25][:, mask_25]
|
| 365 |
+
)
|
| 366 |
+
if np.sum(mask_25) > 0
|
| 367 |
+
else 0.0,
|
| 368 |
+
f"vendi_tm_{prefix}rmsd_2": vendi_from_sim(tm_fixed[mask_2][:, mask_2])
|
| 369 |
+
if np.sum(mask_2) > 0
|
| 370 |
+
else 0.0,
|
| 371 |
+
f"vendi_tm_{prefix}rmsd_4": vendi_from_sim(tm_fixed[mask_4][:, mask_4])
|
| 372 |
+
if np.sum(mask_4) > 0
|
| 373 |
+
else 0.0,
|
| 374 |
+
f"vendi_tm_{prefix}target_aligned_rmsd<2.5": vendi_from_sim(
|
| 375 |
+
tm_fixed[mask_target_25][:, mask_target_25]
|
| 376 |
+
)
|
| 377 |
+
if np.sum(mask_target_25) > 0
|
| 378 |
+
else 0.0,
|
| 379 |
+
}
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
if compute_lddts:
|
| 383 |
+
mask_60 = np.array(
|
| 384 |
+
[m[f"{prefix}designability_lddt_60"] for m in all_metrics]
|
| 385 |
+
)
|
| 386 |
+
mask_65 = np.array(
|
| 387 |
+
[m[f"{prefix}designability_lddt_65"] for m in all_metrics]
|
| 388 |
+
)
|
| 389 |
+
mask_70 = np.array(
|
| 390 |
+
[m[f"{prefix}designability_lddt_70"] for m in all_metrics]
|
| 391 |
+
)
|
| 392 |
+
mask_75 = np.array(
|
| 393 |
+
[m[f"{prefix}designability_lddt_75"] for m in all_metrics]
|
| 394 |
+
)
|
| 395 |
+
mask_80 = np.array(
|
| 396 |
+
[m[f"{prefix}designability_lddt_80"] for m in all_metrics]
|
| 397 |
+
)
|
| 398 |
+
mask_85 = np.array(
|
| 399 |
+
[m[f"{prefix}designability_lddt_85"] for m in all_metrics]
|
| 400 |
+
)
|
| 401 |
+
mask_90 = np.array(
|
| 402 |
+
[m[f"{prefix}designability_lddt_90"] for m in all_metrics]
|
| 403 |
+
)
|
| 404 |
+
scores.update(
|
| 405 |
+
{
|
| 406 |
+
f"vendi_tm_{prefix}lddt_60": vendi_from_sim(
|
| 407 |
+
tm_fixed[mask_60][:, mask_60]
|
| 408 |
+
)
|
| 409 |
+
if np.sum(mask_60) > 0
|
| 410 |
+
else 0.0,
|
| 411 |
+
f"vendi_tm_{prefix}lddt_65": vendi_from_sim(
|
| 412 |
+
tm_fixed[mask_65][:, mask_65]
|
| 413 |
+
)
|
| 414 |
+
if np.sum(mask_65) > 0
|
| 415 |
+
else 0.0,
|
| 416 |
+
f"vendi_tm_{prefix}lddt_70": vendi_from_sim(
|
| 417 |
+
tm_fixed[mask_70][:, mask_70]
|
| 418 |
+
)
|
| 419 |
+
if np.sum(mask_70) > 0
|
| 420 |
+
else 0.0,
|
| 421 |
+
f"vendi_tm_{prefix}lddt_75": vendi_from_sim(
|
| 422 |
+
tm_fixed[mask_75][:, mask_75]
|
| 423 |
+
)
|
| 424 |
+
if np.sum(mask_75) > 0
|
| 425 |
+
else 0.0,
|
| 426 |
+
f"vendi_tm_{prefix}lddt_80": vendi_from_sim(
|
| 427 |
+
tm_fixed[mask_80][:, mask_80]
|
| 428 |
+
)
|
| 429 |
+
if np.sum(mask_80) > 0
|
| 430 |
+
else 0.0,
|
| 431 |
+
f"vendi_tm_{prefix}lddt_85": vendi_from_sim(
|
| 432 |
+
tm_fixed[mask_85][:, mask_85]
|
| 433 |
+
)
|
| 434 |
+
if np.sum(mask_85) > 0
|
| 435 |
+
else 0.0,
|
| 436 |
+
f"vendi_tm_{prefix}lddt_90": vendi_from_sim(
|
| 437 |
+
tm_fixed[mask_90][:, mask_90]
|
| 438 |
+
)
|
| 439 |
+
if np.sum(mask_90) > 0
|
| 440 |
+
else 0.0,
|
| 441 |
+
}
|
| 442 |
+
)
|
| 443 |
+
if compute_iptms:
|
| 444 |
+
mask_80 = np.array([m[f"design_iptm>80"] for m in all_metrics])
|
| 445 |
+
mask_70 = np.array([m[f"design_iptm>70"] for m in all_metrics])
|
| 446 |
+
mask_60 = np.array([m[f"design_iptm>60"] for m in all_metrics])
|
| 447 |
+
mask_50 = np.array([m[f"design_iptm>50"] for m in all_metrics])
|
| 448 |
+
scores.update(
|
| 449 |
+
{
|
| 450 |
+
f"vendi_tm_{prefix}design_iptm_80": vendi_from_sim(
|
| 451 |
+
tm_fixed[mask_80][:, mask_80]
|
| 452 |
+
)
|
| 453 |
+
if np.sum(mask_80) > 0
|
| 454 |
+
else 0.0,
|
| 455 |
+
f"vendi_tm_{prefix}design_iptm_70": vendi_from_sim(
|
| 456 |
+
tm_fixed[mask_70][:, mask_70]
|
| 457 |
+
)
|
| 458 |
+
if np.sum(mask_70) > 0
|
| 459 |
+
else 0.0,
|
| 460 |
+
f"vendi_tm_{prefix}design_iptm_60": vendi_from_sim(
|
| 461 |
+
tm_fixed[mask_60][:, mask_60]
|
| 462 |
+
)
|
| 463 |
+
if np.sum(mask_60) > 0
|
| 464 |
+
else 0.0,
|
| 465 |
+
f"vendi_tm_{prefix}design_iptm_50": vendi_from_sim(
|
| 466 |
+
tm_fixed[mask_50][:, mask_50]
|
| 467 |
+
)
|
| 468 |
+
if np.sum(mask_50) > 0
|
| 469 |
+
else 0.0,
|
| 470 |
+
}
|
| 471 |
+
)
|
| 472 |
+
if compute_min_int_paes:
|
| 473 |
+
mask_15 = np.array([m[f"min_interaction_pae<1.5"] for m in all_metrics])
|
| 474 |
+
mask_2 = np.array([m[f"min_interaction_pae<2"] for m in all_metrics])
|
| 475 |
+
mask_25 = np.array([m[f"min_interaction_pae<2.5"] for m in all_metrics])
|
| 476 |
+
mask_3 = np.array([m[f"min_interaction_pae<3"] for m in all_metrics])
|
| 477 |
+
mask_4 = np.array([m[f"min_interaction_pae<4"] for m in all_metrics])
|
| 478 |
+
mask_5 = np.array([m[f"min_interaction_pae<5"] for m in all_metrics])
|
| 479 |
+
scores.update(
|
| 480 |
+
{
|
| 481 |
+
f"vendi_tm_{prefix}min_interaction_pae_1.5": vendi_from_sim(
|
| 482 |
+
tm_fixed[mask_15][:, mask_15]
|
| 483 |
+
)
|
| 484 |
+
if np.sum(mask_15) > 0
|
| 485 |
+
else 0.0,
|
| 486 |
+
f"vendi_tm_{prefix}min_interaction_pae_2": vendi_from_sim(
|
| 487 |
+
tm_fixed[mask_2][:, mask_2]
|
| 488 |
+
)
|
| 489 |
+
if np.sum(mask_2) > 0
|
| 490 |
+
else 0.0,
|
| 491 |
+
f"vendi_tm_{prefix}min_interaction_pae_2.5": vendi_from_sim(
|
| 492 |
+
tm_fixed[mask_25][:, mask_25]
|
| 493 |
+
)
|
| 494 |
+
if np.sum(mask_25) > 0
|
| 495 |
+
else 0.0,
|
| 496 |
+
f"vendi_tm_{prefix}min_interaction_pae_3": vendi_from_sim(
|
| 497 |
+
tm_fixed[mask_3][:, mask_3]
|
| 498 |
+
)
|
| 499 |
+
if np.sum(mask_3) > 0
|
| 500 |
+
else 0.0,
|
| 501 |
+
f"vendi_tm_{prefix}min_interaction_pae_4": vendi_from_sim(
|
| 502 |
+
tm_fixed[mask_4][:, mask_4]
|
| 503 |
+
)
|
| 504 |
+
if np.sum(mask_4) > 0
|
| 505 |
+
else 0.0,
|
| 506 |
+
f"vendi_tm_{prefix}min_interaction_pae_5": vendi_from_sim(
|
| 507 |
+
tm_fixed[mask_5][:, mask_5]
|
| 508 |
+
)
|
| 509 |
+
if np.sum(mask_5) > 0
|
| 510 |
+
else 0.0,
|
| 511 |
+
}
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
return scores
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def vendi_sequences(all_seqs: List[np.ndarray], diversity_subset: int = None) -> float:
|
| 518 |
+
if diversity_subset is not None and diversity_subset < len(all_seqs):
|
| 519 |
+
all_seqs = random.sample(all_seqs, diversity_subset)
|
| 520 |
+
|
| 521 |
+
N = len(all_seqs)
|
| 522 |
+
sims = np.zeros((N, N), dtype=np.float32)
|
| 523 |
+
aligner = Align.PairwiseAligner()
|
| 524 |
+
for i in tqdm(range(N), desc="Computing sequence diversity."):
|
| 525 |
+
for j in range(i + 1, N):
|
| 526 |
+
seq1 = Seq(all_seqs[i])
|
| 527 |
+
seq2 = Seq(all_seqs[j])
|
| 528 |
+
alignments = aligner.align(seq1, seq2)
|
| 529 |
+
|
| 530 |
+
similarity = alignments[0].score / max(len(seq1), len(seq2))
|
| 531 |
+
sims[i, j] = similarity
|
| 532 |
+
|
| 533 |
+
return {
|
| 534 |
+
"vendi_seq_sim": vendi_from_sim(sims),
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
def compute_novelty_foldseek(
|
| 539 |
+
indir: Path,
|
| 540 |
+
outdir: Path,
|
| 541 |
+
reference_db: Path,
|
| 542 |
+
files: List[str],
|
| 543 |
+
foldseek_binary: str = "/data/rbg/users/hstark/foldseek/bin/foldseek",
|
| 544 |
+
) -> pd.DataFrame:
|
| 545 |
+
if len(files) == 0:
|
| 546 |
+
return np.nan
|
| 547 |
+
|
| 548 |
+
aln_tsv = outdir / "aln.tsv"
|
| 549 |
+
tmp_dir = outdir / "tmp"
|
| 550 |
+
|
| 551 |
+
cmd = [
|
| 552 |
+
foldseek_binary,
|
| 553 |
+
"easy-search",
|
| 554 |
+
str(indir),
|
| 555 |
+
str(reference_db),
|
| 556 |
+
str(aln_tsv),
|
| 557 |
+
str(tmp_dir),
|
| 558 |
+
"--format-output",
|
| 559 |
+
"query,target,alntmscore,qtmscore,ttmscore",
|
| 560 |
+
"--alignment-type",
|
| 561 |
+
"1",
|
| 562 |
+
"--exhaustive-search",
|
| 563 |
+
"1",
|
| 564 |
+
]
|
| 565 |
+
|
| 566 |
+
subprocess.run(cmd, check=True)
|
| 567 |
+
|
| 568 |
+
df = pd.read_csv(
|
| 569 |
+
aln_tsv,
|
| 570 |
+
sep="\t",
|
| 571 |
+
names=["query", "target", "alntmscore", "qtmscore", "ttmscore"],
|
| 572 |
+
)
|
| 573 |
+
df["tmscore"] = (df["qtmscore"] + df["ttmscore"]) / 2
|
| 574 |
+
df = df.groupby("query").max().reset_index()
|
| 575 |
+
queries = [Path(f).stem for f in files]
|
| 576 |
+
df = df.set_index("query").reindex(queries, fill_value=0.0).reset_index()
|
| 577 |
+
df_novelty = df[["query", "tmscore"]].rename(columns={"tmscore": "novelty"})
|
| 578 |
+
return df_novelty
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
def _radius(res_name: str, atom_name: str, element: str) -> float:
|
| 582 |
+
"""
|
| 583 |
+
ProtOr radius with element fallback.
|
| 584 |
+
"""
|
| 585 |
+
try:
|
| 586 |
+
r = vdw_radius_protor(res_name, atom_name)
|
| 587 |
+
if r is not None:
|
| 588 |
+
return r
|
| 589 |
+
except KeyError:
|
| 590 |
+
pass
|
| 591 |
+
r = vdw_radius_single(element)
|
| 592 |
+
return r if r is not None else 1.8
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def compute_sasa(structure_path):
|
| 596 |
+
HYDROPHOBIC_RESIDUES = {"ALA", "VAL", "LEU", "ILE", "MET", "PHE", "PRO", "TRP"}
|
| 597 |
+
atoms = _load_stack(structure_path)[0]
|
| 598 |
+
res_names = [
|
| 599 |
+
bytes(r).decode() if isinstance(r, bytes) else str(r) for r in atoms.res_name
|
| 600 |
+
]
|
| 601 |
+
atom_names = [
|
| 602 |
+
bytes(a).decode() if isinstance(a, bytes) else str(a) for a in atoms.atom_name
|
| 603 |
+
]
|
| 604 |
+
elements = [
|
| 605 |
+
bytes(e).decode() if isinstance(e, bytes) else str(e) for e in atoms.element
|
| 606 |
+
]
|
| 607 |
+
|
| 608 |
+
radii = np.array(
|
| 609 |
+
[
|
| 610 |
+
_radius(rn.strip(), an.strip(), el)
|
| 611 |
+
for rn, an, el in zip(res_names, atom_names, elements)
|
| 612 |
+
],
|
| 613 |
+
dtype=float,
|
| 614 |
+
)
|
| 615 |
+
atom_sasa = sasa(atoms, probe_radius=1.4, point_number=960, vdw_radii=radii)
|
| 616 |
+
mask = (
|
| 617 |
+
np.array([rn.strip() in HYDROPHOBIC_RESIDUES for rn in res_names])
|
| 618 |
+
& np.char.startswith(atom_names, "C")
|
| 619 |
+
& (atom_sasa > 0)
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
return atoms.coord[mask], atom_sasa[mask]
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
def largest_hydrophobic_patch_area(cif_path, distance_cutoff=6.0):
|
| 626 |
+
result = compute_sasa(cif_path)
|
| 627 |
+
if result is None or result[0].size == 0:
|
| 628 |
+
return np.nan
|
| 629 |
+
|
| 630 |
+
coords, sasa_vals = result
|
| 631 |
+
if len(coords) == 0:
|
| 632 |
+
return 0.0
|
| 633 |
+
clustering = DBSCAN(eps=distance_cutoff, min_samples=1).fit(coords)
|
| 634 |
+
labels = clustering.labels_
|
| 635 |
+
|
| 636 |
+
max_patch_area = 0.0
|
| 637 |
+
for label in np.unique(labels):
|
| 638 |
+
area = sasa_vals[labels == label].sum()
|
| 639 |
+
max_patch_area = max(max_patch_area, area)
|
| 640 |
+
|
| 641 |
+
return max_patch_area
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
def get_delta_sasa(
|
| 645 |
+
path,
|
| 646 |
+
atom_target_mask,
|
| 647 |
+
atom_design_mask,
|
| 648 |
+
):
|
| 649 |
+
stack = _load_stack(path)
|
| 650 |
+
atoms = stack[0]
|
| 651 |
+
|
| 652 |
+
res = [
|
| 653 |
+
r.decode().strip() if isinstance(r, bytes) else str(r).strip()
|
| 654 |
+
for r in atoms.res_name
|
| 655 |
+
]
|
| 656 |
+
atm = [
|
| 657 |
+
a.decode().strip() if isinstance(a, bytes) else str(a).strip()
|
| 658 |
+
for a in atoms.atom_name
|
| 659 |
+
]
|
| 660 |
+
elem = [e.decode() if isinstance(e, bytes) else str(e) for e in atoms.element]
|
| 661 |
+
|
| 662 |
+
radii = np.array(
|
| 663 |
+
[_radius(rn, an, el) for rn, an, el in zip(res, atm, elem)], dtype=float
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
bound_mask = atom_design_mask | atom_target_mask
|
| 668 |
+
atoms_bound = atoms[bound_mask]
|
| 669 |
+
radii_bound = radii[bound_mask]
|
| 670 |
+
|
| 671 |
+
area_bound = sasa(
|
| 672 |
+
atoms_bound,
|
| 673 |
+
probe_radius=1.4,
|
| 674 |
+
point_number=960,
|
| 675 |
+
vdw_radii=radii_bound,
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
target_in_bound = atom_target_mask[bound_mask]
|
| 679 |
+
target_bound = area_bound[target_in_bound].sum()
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
target_atoms = atoms[atom_target_mask]
|
| 684 |
+
target_res = [r for r, m in zip(res, atom_target_mask) if m]
|
| 685 |
+
target_atm = [a for a, m in zip(atm, atom_target_mask) if m]
|
| 686 |
+
target_elem = [e for e, m in zip(elem, atom_target_mask) if m]
|
| 687 |
+
|
| 688 |
+
radii_lig = np.array(
|
| 689 |
+
[_radius(rn, an, el) for rn, an, el in zip(target_res, target_atm, target_elem)],
|
| 690 |
+
dtype=float,
|
| 691 |
+
)
|
| 692 |
+
target_area = sasa(
|
| 693 |
+
target_atoms,
|
| 694 |
+
probe_radius=1.4,
|
| 695 |
+
point_number=960,
|
| 696 |
+
vdw_radii=radii_lig,
|
| 697 |
+
)
|
| 698 |
+
delta = target_area.sum() - target_bound
|
| 699 |
+
return delta, target_area.sum(), target_bound
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
def compute_ss_metrics(dssp_pred, ss_conditioning_metricsed):
|
| 703 |
+
ss_metrics = {}
|
| 704 |
+
conditioned_mask = ss_conditioning_metricsed != 0
|
| 705 |
+
if conditioned_mask.sum() == 0:
|
| 706 |
+
return {
|
| 707 |
+
"precision_loop": float("nan"),
|
| 708 |
+
"recall_loop": float("nan"),
|
| 709 |
+
"accuracy_loop": float("nan"),
|
| 710 |
+
"precision_helix": float("nan"),
|
| 711 |
+
"recall_helix": float("nan"),
|
| 712 |
+
"accuracy_helix": float("nan"),
|
| 713 |
+
"precision_sheet": float("nan"),
|
| 714 |
+
"recall_sheet": float("nan"),
|
| 715 |
+
"accuracy_sheet": float("nan"),
|
| 716 |
+
"accuracy_overall": float("nan"),
|
| 717 |
+
}
|
| 718 |
+
types = {1: "loop", 2: "helix", 3: "sheet"}
|
| 719 |
+
TP_total, total_conditioned = 0, conditioned_mask.sum().item()
|
| 720 |
+
for i, name in types.items():
|
| 721 |
+
TP = ((dssp_pred == i) & (ss_conditioning_metricsed == i)).sum().item()
|
| 722 |
+
FP = (
|
| 723 |
+
((dssp_pred == i) & (ss_conditioning_metricsed != i) & conditioned_mask)
|
| 724 |
+
.sum()
|
| 725 |
+
.item()
|
| 726 |
+
)
|
| 727 |
+
FN = ((dssp_pred != i) & (ss_conditioning_metricsed == i)).sum().item()
|
| 728 |
+
precision = TP / (TP + FP) if (TP + FP) > 0 else float("nan")
|
| 729 |
+
recall = TP / (TP + FN) if (TP + FN) > 0 else float("nan")
|
| 730 |
+
accuracy = TP / (TP + FP + FN) if (TP + FP + FN) > 0 else float("nan")
|
| 731 |
+
ss_metrics[f"precision_{name}"] = precision
|
| 732 |
+
ss_metrics[f"recall_{name}"] = recall
|
| 733 |
+
ss_metrics[f"accuracy_{name}"] = accuracy
|
| 734 |
+
TP_total += TP
|
| 735 |
+
|
| 736 |
+
accuracy_overall = (
|
| 737 |
+
TP_total / total_conditioned if total_conditioned > 0 else float("nan")
|
| 738 |
+
)
|
| 739 |
+
ss_metrics["accuracy_overall"] = accuracy_overall
|
| 740 |
+
return ss_metrics
|
| 741 |
+
|
| 742 |
+
|
| 743 |
+
def filter_resolved_atoms(structure: Structure) -> Structure:
|
| 744 |
+
resolved_atom_indices = np.where(structure.atoms["is_present"])[0]
|
| 745 |
+
return Structure.extract_atoms(structure, resolved_atom_indices)
|
| 746 |
+
|
| 747 |
+
|
| 748 |
+
def save_design_only_structure_to_cif(atom_design_mask, structure, output_path: Path):
|
| 749 |
+
design_atom_indices = torch.where(atom_design_mask)[0].cpu().numpy()
|
| 750 |
+
design_only_str = Structure.extract_atoms(
|
| 751 |
+
structure, design_atom_indices, res_reindex=True
|
| 752 |
+
)
|
| 753 |
+
cif_text = to_mmcif(design_only_str)
|
| 754 |
+
output_path.write_text(cif_text)
|
| 755 |
+
return cif_text
|
| 756 |
+
|
| 757 |
+
|
| 758 |
+
def save_design_only_structure_to_pdb(atom_design_mask, structure, output_path: Path):
|
| 759 |
+
cif_path = output_path.with_suffix(".cif")
|
| 760 |
+
cif_text = save_design_only_structure_to_cif(atom_design_mask, structure, cif_path)
|
| 761 |
+
|
| 762 |
+
cif_io = io.StringIO(cif_text)
|
| 763 |
+
mmcif_parser = PDB.MMCIFParser()
|
| 764 |
+
pdb_writer = PDB.PDBIO()
|
| 765 |
+
parsed_structure = mmcif_parser.get_structure("des_only", cif_io)
|
| 766 |
+
pdb_writer.set_structure(parsed_structure)
|
| 767 |
+
pdb_writer.save(str(output_path))
|
| 768 |
+
|
| 769 |
+
|
| 770 |
+
########################################################################################################
|
| 771 |
+
# Hydrophobycity computation functions. From Jeremie Alexander.
|
| 772 |
+
########################################################################################################
|
| 773 |
+
|
| 774 |
+
|
| 775 |
+
def calc_base_h(seq: str) -> float:
|
| 776 |
+
s = seq.upper()
|
| 777 |
+
n = len(s)
|
| 778 |
+
if n == 0:
|
| 779 |
+
return 0.0
|
| 780 |
+
H = 0.0
|
| 781 |
+
|
| 782 |
+
# position-specific coefficients
|
| 783 |
+
for i, aa in enumerate(s):
|
| 784 |
+
if aa not in const.hydrophobicity_info:
|
| 785 |
+
raise ValueError(f"Unknown residue '{aa}' in '{seq}'")
|
| 786 |
+
if i == 0:
|
| 787 |
+
key = "Rc1"
|
| 788 |
+
elif i == 1:
|
| 789 |
+
key = "Rc2"
|
| 790 |
+
elif i == n - 1:
|
| 791 |
+
key = "Rn"
|
| 792 |
+
elif i == n - 2:
|
| 793 |
+
key = "Rn1"
|
| 794 |
+
else:
|
| 795 |
+
key = "Rc"
|
| 796 |
+
H += const.hydrophobicity_info[aa][key]
|
| 797 |
+
|
| 798 |
+
# nearest-neighbor penalties around H/R/K
|
| 799 |
+
for i, aa in enumerate(s):
|
| 800 |
+
if aa in ("H", "R", "K"):
|
| 801 |
+
for j in (i - 1, i + 1):
|
| 802 |
+
if 0 <= j < n and s[j] in const.nn_penalty:
|
| 803 |
+
H -= const.nn_penalty[s[j]]
|
| 804 |
+
|
| 805 |
+
# proline run penalties
|
| 806 |
+
i = 0
|
| 807 |
+
while i < n:
|
| 808 |
+
if s[i] == "P":
|
| 809 |
+
j = i
|
| 810 |
+
while j < n and s[j] == "P":
|
| 811 |
+
j += 1
|
| 812 |
+
run = j - i
|
| 813 |
+
if run >= 4:
|
| 814 |
+
H -= 5.0
|
| 815 |
+
elif run == 3:
|
| 816 |
+
H -= 3.5
|
| 817 |
+
elif run == 2:
|
| 818 |
+
H -= 1.2
|
| 819 |
+
i = j
|
| 820 |
+
else:
|
| 821 |
+
i += 1
|
| 822 |
+
return H
|
| 823 |
+
|
| 824 |
+
|
| 825 |
+
def apply_length_weight(H: float, n: int) -> float:
|
| 826 |
+
if n < 8:
|
| 827 |
+
KL = 1.0 - 0.055 * (8 - n)
|
| 828 |
+
elif n > 20:
|
| 829 |
+
KL = 1.0 / (1.0 + 0.027 * (n - 20))
|
| 830 |
+
else:
|
| 831 |
+
KL = 1.0
|
| 832 |
+
return H * KL
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
def overall_penalty(H: float) -> float:
|
| 836 |
+
if H <= 20:
|
| 837 |
+
return H
|
| 838 |
+
if H <= 30:
|
| 839 |
+
return H - 0.27 * (H - 18.0)
|
| 840 |
+
if H <= 40:
|
| 841 |
+
return H - 0.33 * (H - 18.0)
|
| 842 |
+
if H <= 50:
|
| 843 |
+
return H - 0.38 * (H - 18.0)
|
| 844 |
+
return H - 0.447 * (H - 18.0)
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
def calc_hydrophobicity(seq: str) -> float:
|
| 848 |
+
s = (seq or "").strip().upper()
|
| 849 |
+
if not s or "X" in s:
|
| 850 |
+
return float("nan")
|
| 851 |
+
base = calc_base_h(s)
|
| 852 |
+
base = apply_length_weight(base, len(s))
|
| 853 |
+
return round(overall_penalty(base), 4)
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
########################################################################################################
|
| 857 |
+
# Liability computation functions
|
| 858 |
+
########################################################################################################
|
| 859 |
+
|
| 860 |
+
|
| 861 |
+
def get_motif_set(modality="antibody", peptide_type="linear"):
|
| 862 |
+
"""
|
| 863 |
+
Returns a dict of motif_name -> compiled regex for the given modality.
|
| 864 |
+
modality: 'antibody' or 'peptide'
|
| 865 |
+
peptide_type: 'linear' or 'cyclic' (only for peptide modality)
|
| 866 |
+
|
| 867 |
+
|
| 868 |
+
"""
|
| 869 |
+
if modality == "antibody":
|
| 870 |
+
return {
|
| 871 |
+
"DeAmdH": re.compile(r"N[GS]"), # High-risk deamidation
|
| 872 |
+
"DeAmdM": re.compile(r"N[AHNT]"), # Medium-risk deamidation
|
| 873 |
+
"DeAmdL": re.compile(r"[STK]N"), # Low-risk deamidation
|
| 874 |
+
"Ngly": re.compile(r"N[^P][ST]"), # N-glycosylation sites
|
| 875 |
+
"Isom": re.compile(r"D[DG HST]".replace(" ", "")), # Isomerization
|
| 876 |
+
"Isomer": re.compile(r"DG|DS|DD"), # Isomerization variants
|
| 877 |
+
"FragH": re.compile(r"DP"), # High fragmentation risk
|
| 878 |
+
"FragM": re.compile(r"TS"), # Medium fragmentation risk
|
| 879 |
+
"TrpOx": re.compile(r"W"), # Tryptophan oxidation
|
| 880 |
+
"MetOx": re.compile(r"M"), # Methionine oxidation
|
| 881 |
+
"Hydro": re.compile(r"NP"), # Hydrolysis prone
|
| 882 |
+
"IntBind": re.compile(r"GPR|RGD|RYD|LDV|DGE|KGD|NGR"), # Integrin binding
|
| 883 |
+
"Polyreactive": re.compile(
|
| 884 |
+
r"GGG|GG|RR|VG|VVV|WWW|YY|WxW"
|
| 885 |
+
), # Polyreactivity
|
| 886 |
+
"AggPatch": re.compile(r"FHW"), # Aggregation patches
|
| 887 |
+
"ViscPatch": re.compile(r"HYF|HWH"), # Viscosity patches
|
| 888 |
+
"DeAmdH": re.compile(r"N[GS]"),
|
| 889 |
+
"DeAmdM": re.compile(r"N[AHNT]"),
|
| 890 |
+
"DeAmdL": re.compile(r"[STK]N"),
|
| 891 |
+
"Ngly": re.compile(r"N[^P][ST]"),
|
| 892 |
+
"Isom": re.compile(r"D[DG HST]".replace(" ", "")),
|
| 893 |
+
"Isomer": re.compile(r"DG|DS|DD"),
|
| 894 |
+
"FragH": re.compile(r"DP"),
|
| 895 |
+
"FragM": re.compile(r"TS"),
|
| 896 |
+
"TrpOx": re.compile(r"W"),
|
| 897 |
+
"MetOx": re.compile(r"M"),
|
| 898 |
+
"Hydro": re.compile(r"NP"),
|
| 899 |
+
"IntBind": re.compile(r"GPR|RGD|RYD|LDV|DGE|KGD|NGR"),
|
| 900 |
+
"Polyreactive": re.compile(r"GGG|GG|RR|VG|VVV|WWW|YY|WxW"),
|
| 901 |
+
"AggPatch": re.compile(r"FHW"),
|
| 902 |
+
"ViscPatch": re.compile(r"HYF|HWH"),
|
| 903 |
+
}
|
| 904 |
+
elif modality == "peptide":
|
| 905 |
+
motifs = {
|
| 906 |
+
"AspBridge": re.compile(r"N[GSQA]"), # Deamidation hotspots
|
| 907 |
+
"AspCleave": re.compile(r"D[PGS]"), # Acidic cleavage sites
|
| 908 |
+
"NTCycl": re.compile(r"^[QN]"), # N-terminal cyclization
|
| 909 |
+
"ProtTryp": re.compile(r"[KR](?=.)"), # Trypsin cleavage sites
|
| 910 |
+
"DPP4": re.compile(r"^[PX]?[AP]"), # DPP4 cleavage sites
|
| 911 |
+
"MetOx": re.compile(r"M"), # Methionine oxidation
|
| 912 |
+
"TrpOx": re.compile(r"W"), # Tryptophan oxidation
|
| 913 |
+
"HydroPatch": re.compile(r"[FILVWY]{3,}"), # Hydrophobic patches
|
| 914 |
+
}
|
| 915 |
+
if peptide_type == "cyclic":
|
| 916 |
+
# remove N-term liabilities for cyclic peptides
|
| 917 |
+
motifs.pop("NTCycl", None)
|
| 918 |
+
motifs.pop("DPP4", None)
|
| 919 |
+
elif peptide_type == "linear":
|
| 920 |
+
# For linear peptides, we handle cysteine pairing separately
|
| 921 |
+
# so we don't include CysOx in the motif set
|
| 922 |
+
pass
|
| 923 |
+
return motifs
|
| 924 |
+
else:
|
| 925 |
+
raise ValueError(f"Unknown modality: {modality}")
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
def severity_score(name):
|
| 929 |
+
return const.liability_severity.get(name, const.default_severity)
|
| 930 |
+
|
| 931 |
+
|
| 932 |
+
def compute_liability_scores(sequences, modality="antibody", peptide_type="linear"):
|
| 933 |
+
"""
|
| 934 |
+
Compute liability scores for given sequences.
|
| 935 |
+
modality: 'antibody' or 'peptide'; peptide_type: 'linear' or 'cyclic'.
|
| 936 |
+
For cyclic peptides, terminal CysOx flags are skipped.
|
| 937 |
+
|
| 938 |
+
Returns:
|
| 939 |
+
dict: sequence -> {'score': int, 'violations': list of dicts}
|
| 940 |
+
"""
|
| 941 |
+
motifs = get_motif_set(modality, peptide_type)
|
| 942 |
+
results = {}
|
| 943 |
+
for seq in sequences:
|
| 944 |
+
violations = []
|
| 945 |
+
total_score = 0
|
| 946 |
+
length = len(seq)
|
| 947 |
+
# motif scanning
|
| 948 |
+
for name, pat in motifs.items():
|
| 949 |
+
for m in pat.finditer(seq):
|
| 950 |
+
pos = m.start() + 1
|
| 951 |
+
# skip terminal cysteines for cyclic peptides
|
| 952 |
+
if (
|
| 953 |
+
modality == "peptide"
|
| 954 |
+
and peptide_type == "cyclic"
|
| 955 |
+
and name == "CysOx"
|
| 956 |
+
and pos in (1, length)
|
| 957 |
+
):
|
| 958 |
+
continue
|
| 959 |
+
sev = severity_score(name)
|
| 960 |
+
violations.append(
|
| 961 |
+
{"motif": name, "pos": pos, "len": len(m.group()), "severity": sev}
|
| 962 |
+
)
|
| 963 |
+
total_score += sev
|
| 964 |
+
# antibody-specific extras
|
| 965 |
+
if modality == "antibody":
|
| 966 |
+
# unpaired cysteines
|
| 967 |
+
cpos = [i for i, aa in enumerate(seq) if aa == "C"]
|
| 968 |
+
paired = set()
|
| 969 |
+
for i in range(len(cpos) - 1):
|
| 970 |
+
if abs(cpos[i + 1] - cpos[i]) in (1, 2):
|
| 971 |
+
paired.update({cpos[i], cpos[i + 1]})
|
| 972 |
+
for i in cpos:
|
| 973 |
+
if i not in paired:
|
| 974 |
+
sev = severity_score("UnpairedCys")
|
| 975 |
+
violations.append(
|
| 976 |
+
{
|
| 977 |
+
"motif": "UnpairedCys",
|
| 978 |
+
"pos": i + 1,
|
| 979 |
+
"len": 1,
|
| 980 |
+
"severity": sev,
|
| 981 |
+
}
|
| 982 |
+
)
|
| 983 |
+
total_score += sev
|
| 984 |
+
# net charge
|
| 985 |
+
charge = seq.count("K") + seq.count("R") - seq.count("D") - seq.count("E")
|
| 986 |
+
if charge > 1:
|
| 987 |
+
sev = const.default_severity
|
| 988 |
+
violations.append(
|
| 989 |
+
{"motif": "HighNetCharge", "pos": None, "len": 0, "severity": sev}
|
| 990 |
+
)
|
| 991 |
+
total_score += sev
|
| 992 |
+
# peptide-specific extras
|
| 993 |
+
elif modality == "peptide":
|
| 994 |
+
# For linear peptides, only flag unpaired cysteines (odd number of cysteines)
|
| 995 |
+
# For cyclic peptides, terminal cysteines are expected, so only flag internal unpaired cysteines
|
| 996 |
+
cpos = [i for i, aa in enumerate(seq) if aa == "C"]
|
| 997 |
+
if peptide_type == "linear":
|
| 998 |
+
# For linear peptides, if there's an odd number of cysteines, flag all cysteines as potential liabilities
|
| 999 |
+
# since we don't know which one is unpaired
|
| 1000 |
+
if len(cpos) % 2 == 1:
|
| 1001 |
+
sev = severity_score("UnpairedCys")
|
| 1002 |
+
for cys_pos in cpos:
|
| 1003 |
+
violations.append(
|
| 1004 |
+
{
|
| 1005 |
+
"motif": "UnpairedCys",
|
| 1006 |
+
"pos": cys_pos + 1,
|
| 1007 |
+
"len": 1,
|
| 1008 |
+
"severity": sev,
|
| 1009 |
+
}
|
| 1010 |
+
)
|
| 1011 |
+
total_score += sev
|
| 1012 |
+
elif peptide_type == "cyclic":
|
| 1013 |
+
# For cyclic peptides, terminal cysteines are expected for cyclization
|
| 1014 |
+
# Only flag internal unpaired cysteines
|
| 1015 |
+
internal_cpos = [
|
| 1016 |
+
i for i in cpos if i != 0 and i != len(seq) - 1
|
| 1017 |
+
] # exclude terminal positions
|
| 1018 |
+
if len(internal_cpos) % 2 == 1:
|
| 1019 |
+
# Flag all internal cysteines as potential liabilities
|
| 1020 |
+
sev = severity_score("UnpairedCys")
|
| 1021 |
+
for cys_pos in internal_cpos:
|
| 1022 |
+
violations.append(
|
| 1023 |
+
{
|
| 1024 |
+
"motif": "UnpairedCys",
|
| 1025 |
+
"pos": cys_pos + 1,
|
| 1026 |
+
"len": 1,
|
| 1027 |
+
"severity": sev,
|
| 1028 |
+
}
|
| 1029 |
+
)
|
| 1030 |
+
total_score += sev
|
| 1031 |
+
|
| 1032 |
+
# Additional liability checks for cyclic peptides
|
| 1033 |
+
# 1. Check for low hydrophilic content (< 40%)
|
| 1034 |
+
hydrophilic_residues = (
|
| 1035 |
+
seq.count("D")
|
| 1036 |
+
+ seq.count("E")
|
| 1037 |
+
+ seq.count("K")
|
| 1038 |
+
+ seq.count("R")
|
| 1039 |
+
+ seq.count("H")
|
| 1040 |
+
+ seq.count("N")
|
| 1041 |
+
+ seq.count("Q")
|
| 1042 |
+
+ seq.count("S")
|
| 1043 |
+
+ seq.count("T")
|
| 1044 |
+
)
|
| 1045 |
+
hydrophilic_percentage = (hydrophilic_residues / len(seq)) * 100
|
| 1046 |
+
if hydrophilic_percentage < 40:
|
| 1047 |
+
sev = severity_score("LowHydrophilic")
|
| 1048 |
+
violations.append(
|
| 1049 |
+
{
|
| 1050 |
+
"motif": "LowHydrophilic",
|
| 1051 |
+
"pos": None,
|
| 1052 |
+
"len": 0,
|
| 1053 |
+
"severity": sev,
|
| 1054 |
+
"details": f"{hydrophilic_percentage:.1f}% hydrophilic",
|
| 1055 |
+
}
|
| 1056 |
+
)
|
| 1057 |
+
total_score += sev
|
| 1058 |
+
|
| 1059 |
+
# 2. Check for consecutive identical residues
|
| 1060 |
+
max_consec_identical = 1
|
| 1061 |
+
current_consec = 1
|
| 1062 |
+
for i in range(1, len(seq)):
|
| 1063 |
+
if seq[i] == seq[i - 1]:
|
| 1064 |
+
current_consec += 1
|
| 1065 |
+
max_consec_identical = max(max_consec_identical, current_consec)
|
| 1066 |
+
else:
|
| 1067 |
+
current_consec = 1
|
| 1068 |
+
|
| 1069 |
+
if max_consec_identical > 1:
|
| 1070 |
+
sev = severity_score("ConsecIdentical")
|
| 1071 |
+
violations.append(
|
| 1072 |
+
{
|
| 1073 |
+
"motif": "ConsecIdentical",
|
| 1074 |
+
"pos": None,
|
| 1075 |
+
"len": 0,
|
| 1076 |
+
"severity": sev,
|
| 1077 |
+
"details": f"{max_consec_identical} consecutive identical",
|
| 1078 |
+
}
|
| 1079 |
+
)
|
| 1080 |
+
total_score += sev
|
| 1081 |
+
|
| 1082 |
+
# 3. Check for more than 4 consecutive hydrophobic residues
|
| 1083 |
+
max_consec_hydrophobic = 0
|
| 1084 |
+
current_consec = 0
|
| 1085 |
+
for aa in seq:
|
| 1086 |
+
if aa in "FILVWY":
|
| 1087 |
+
current_consec += 1
|
| 1088 |
+
max_consec_hydrophobic = max(
|
| 1089 |
+
max_consec_hydrophobic, current_consec
|
| 1090 |
+
)
|
| 1091 |
+
else:
|
| 1092 |
+
current_consec = 0
|
| 1093 |
+
|
| 1094 |
+
if max_consec_hydrophobic > 4:
|
| 1095 |
+
sev = severity_score("LongHydrophobic")
|
| 1096 |
+
violations.append(
|
| 1097 |
+
{
|
| 1098 |
+
"motif": "LongHydrophobic",
|
| 1099 |
+
"pos": None,
|
| 1100 |
+
"len": 0,
|
| 1101 |
+
"severity": sev,
|
| 1102 |
+
"details": f"{max_consec_hydrophobic} consecutive hydrophobic",
|
| 1103 |
+
}
|
| 1104 |
+
)
|
| 1105 |
+
total_score += sev
|
| 1106 |
+
results[seq] = {"score": total_score, "violations": violations}
|
| 1107 |
+
return results
|
| 1108 |
+
|
| 1109 |
+
|
| 1110 |
+
def compute_liability_metrics(sequence, liability_modality, liability_peptide_type):
|
| 1111 |
+
metrics = {}
|
| 1112 |
+
# check if sequence is valid
|
| 1113 |
+
if not sequence or len(sequence) == 0:
|
| 1114 |
+
raise ValueError(f"Sequence is empty: '{sequence}'")
|
| 1115 |
+
|
| 1116 |
+
liability_results = compute_liability_scores(
|
| 1117 |
+
[sequence],
|
| 1118 |
+
modality=liability_modality,
|
| 1119 |
+
peptide_type=liability_peptide_type,
|
| 1120 |
+
)
|
| 1121 |
+
liability_data = liability_results[sequence]
|
| 1122 |
+
|
| 1123 |
+
# Store liability metrics
|
| 1124 |
+
metrics["liability_score"] = liability_data["score"]
|
| 1125 |
+
metrics["liability_num_violations"] = len(liability_data["violations"])
|
| 1126 |
+
|
| 1127 |
+
# Count violations by severity
|
| 1128 |
+
high_severity_violations = [
|
| 1129 |
+
v for v in liability_data["violations"] if v["severity"] >= 10
|
| 1130 |
+
]
|
| 1131 |
+
medium_severity_violations = [
|
| 1132 |
+
v for v in liability_data["violations"] if 5 <= v["severity"] < 10
|
| 1133 |
+
]
|
| 1134 |
+
low_severity_violations = [
|
| 1135 |
+
v for v in liability_data["violations"] if v["severity"] < 5
|
| 1136 |
+
]
|
| 1137 |
+
|
| 1138 |
+
metrics["liability_high_severity_violations"] = len(high_severity_violations)
|
| 1139 |
+
metrics["liability_medium_severity_violations"] = len(medium_severity_violations)
|
| 1140 |
+
metrics["liability_low_severity_violations"] = len(low_severity_violations)
|
| 1141 |
+
|
| 1142 |
+
# Count violations by type
|
| 1143 |
+
violation_counts = {}
|
| 1144 |
+
for v in liability_data["violations"]:
|
| 1145 |
+
motif = v["motif"]
|
| 1146 |
+
violation_counts[motif] = violation_counts.get(motif, 0) + 1
|
| 1147 |
+
|
| 1148 |
+
# Store individual violation type counts as metrics
|
| 1149 |
+
for motif, count in violation_counts.items():
|
| 1150 |
+
metrics[f"liability_{motif}_count"] = count
|
| 1151 |
+
|
| 1152 |
+
# Add detailed violation information
|
| 1153 |
+
# Group violations by type for intelligent reporting
|
| 1154 |
+
violations_by_type = {}
|
| 1155 |
+
for v in liability_data["violations"]:
|
| 1156 |
+
motif = v["motif"]
|
| 1157 |
+
if motif not in violations_by_type:
|
| 1158 |
+
violations_by_type[motif] = []
|
| 1159 |
+
violations_by_type[motif].append(v)
|
| 1160 |
+
|
| 1161 |
+
# Initialize default values for all motifs to ensure consistent dataframe columns
|
| 1162 |
+
# Use the full motif set for the configured modality/peptide_type so columns are consistent
|
| 1163 |
+
all_motifs = set(
|
| 1164 |
+
get_motif_set(
|
| 1165 |
+
modality=liability_modality,
|
| 1166 |
+
peptide_type=liability_peptide_type,
|
| 1167 |
+
).keys()
|
| 1168 |
+
)
|
| 1169 |
+
for motif in all_motifs:
|
| 1170 |
+
# Initialize all possible fields with default values
|
| 1171 |
+
metrics[f"liability_{motif}_count"] = 0
|
| 1172 |
+
metrics[
|
| 1173 |
+
f"liability_{motif}_position"
|
| 1174 |
+
] = -1 # use -1 for no position (keeps int dtype)
|
| 1175 |
+
metrics[f"liability_{motif}_length"] = 0
|
| 1176 |
+
metrics[f"liability_{motif}_severity"] = 0
|
| 1177 |
+
metrics[f"liability_{motif}_details"] = ""
|
| 1178 |
+
metrics[f"liability_{motif}_positions"] = ""
|
| 1179 |
+
metrics[f"liability_{motif}_num_positions"] = 0
|
| 1180 |
+
metrics[f"liability_{motif}_global_details"] = ""
|
| 1181 |
+
metrics[f"liability_{motif}_avg_severity"] = 0.0
|
| 1182 |
+
|
| 1183 |
+
# Store detailed violation information
|
| 1184 |
+
for motif, motif_violations in violations_by_type.items():
|
| 1185 |
+
if len(motif_violations) == 1:
|
| 1186 |
+
# Single violation - store all details
|
| 1187 |
+
v = motif_violations[0]
|
| 1188 |
+
# Ensure position is an integer; use -1 for non-positional violations
|
| 1189 |
+
metrics[f"liability_{motif}_position"] = (
|
| 1190 |
+
int(v["pos"]) if v["pos"] is not None else -1
|
| 1191 |
+
)
|
| 1192 |
+
metrics[f"liability_{motif}_length"] = v["len"]
|
| 1193 |
+
metrics[f"liability_{motif}_severity"] = v["severity"]
|
| 1194 |
+
if "details" in v:
|
| 1195 |
+
metrics[f"liability_{motif}_details"] = v["details"]
|
| 1196 |
+
else:
|
| 1197 |
+
metrics[f"liability_{motif}_details"] = ""
|
| 1198 |
+
else:
|
| 1199 |
+
# Multiple violations - store summary information
|
| 1200 |
+
positions = [v["pos"] for v in motif_violations if v["pos"] is not None]
|
| 1201 |
+
global_violations = [v for v in motif_violations if v["pos"] is None]
|
| 1202 |
+
|
| 1203 |
+
if positions:
|
| 1204 |
+
# Store position range for positional violations
|
| 1205 |
+
metrics[f"liability_{motif}_positions"] = (
|
| 1206 |
+
f"{min(positions)}-{max(positions)}"
|
| 1207 |
+
)
|
| 1208 |
+
metrics[f"liability_{motif}_num_positions"] = len(positions)
|
| 1209 |
+
else:
|
| 1210 |
+
metrics[f"liability_{motif}_positions"] = ""
|
| 1211 |
+
metrics[f"liability_{motif}_num_positions"] = 0
|
| 1212 |
+
|
| 1213 |
+
if global_violations:
|
| 1214 |
+
# Store details for global violations
|
| 1215 |
+
details = [
|
| 1216 |
+
v.get("details", "") for v in global_violations if v.get("details")
|
| 1217 |
+
]
|
| 1218 |
+
if details:
|
| 1219 |
+
metrics[f"liability_{motif}_global_details"] = "; ".join(details)
|
| 1220 |
+
else:
|
| 1221 |
+
metrics[f"liability_{motif}_global_details"] = ""
|
| 1222 |
+
else:
|
| 1223 |
+
metrics[f"liability_{motif}_global_details"] = ""
|
| 1224 |
+
|
| 1225 |
+
# Store average severity
|
| 1226 |
+
avg_severity = sum(v["severity"] for v in motif_violations) / len(
|
| 1227 |
+
motif_violations
|
| 1228 |
+
)
|
| 1229 |
+
metrics[f"liability_{motif}_avg_severity"] = round(avg_severity, 1)
|
| 1230 |
+
|
| 1231 |
+
# Add a comprehensive violation summary for easy interpretation
|
| 1232 |
+
violation_summary = []
|
| 1233 |
+
for motif, motif_violations in violations_by_type.items():
|
| 1234 |
+
count = len(motif_violations)
|
| 1235 |
+
if count == 1:
|
| 1236 |
+
v = motif_violations[0]
|
| 1237 |
+
if v["pos"] is not None:
|
| 1238 |
+
violation_summary.append(f"{motif}(pos{v['pos']},sev{v['severity']})")
|
| 1239 |
+
else:
|
| 1240 |
+
details = v.get("details", "")
|
| 1241 |
+
violation_summary.append(
|
| 1242 |
+
f"{motif}({details},sev{v['severity']})"
|
| 1243 |
+
if details
|
| 1244 |
+
else f"{motif}(sev{v['severity']})"
|
| 1245 |
+
)
|
| 1246 |
+
else:
|
| 1247 |
+
positions = [v["pos"] for v in motif_violations if v["pos"] is not None]
|
| 1248 |
+
if positions:
|
| 1249 |
+
violation_summary.append(
|
| 1250 |
+
f"{motif}x{count}(pos{min(positions)}-{max(positions)},sev{motif_violations[0]['severity']})"
|
| 1251 |
+
)
|
| 1252 |
+
else:
|
| 1253 |
+
violation_summary.append(
|
| 1254 |
+
f"{motif}x{count}(sev{motif_violations[0]['severity']})"
|
| 1255 |
+
)
|
| 1256 |
+
|
| 1257 |
+
metrics["liability_violations_summary"] = (
|
| 1258 |
+
"; ".join(violation_summary) if violation_summary else ""
|
| 1259 |
+
)
|
| 1260 |
+
return metrics
|
model/boltzgen/task/filter/__init__.py
ADDED
|
File without changes
|
model/boltzgen/task/filter/filter.py
ADDED
|
@@ -0,0 +1,1413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from boltzgen.utils.quiet import quiet_startup
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
quiet_startup()
|
| 6 |
+
from typing import Dict
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import matplotlib.pyplot as plt
|
| 10 |
+
import shutil
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
from matplotlib.backends.backend_pdf import PdfPages
|
| 13 |
+
import pandas as pd
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from Bio import Align
|
| 16 |
+
import numpy as np
|
| 17 |
+
import random
|
| 18 |
+
from tqdm.auto import tqdm
|
| 19 |
+
import heapq
|
| 20 |
+
from textwrap import fill, dedent
|
| 21 |
+
import re
|
| 22 |
+
|
| 23 |
+
from boltzgen.task.analyze.analyze_utils import compute_liability_scores
|
| 24 |
+
from boltzgen.task.filter.seqplot_utils import (
|
| 25 |
+
aa_composition_pie,
|
| 26 |
+
cdr_logo,
|
| 27 |
+
create_alignment_logo,
|
| 28 |
+
plot_seq_liabilities,
|
| 29 |
+
)
|
| 30 |
+
from boltzgen.task.task import Task
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Filter(Task):
|
| 34 |
+
"""
|
| 35 |
+
This step consumes the aggregated metrics produced by **Analyze** (i.e.
|
| 36 |
+
``aggregate_metrics_*.csv`` in ``design_dir``), applies hard
|
| 37 |
+
filtering thresholds, ranks candidates by a composite quality key, and then
|
| 38 |
+
performs a lazy-greedy **diversity selection** over sequence identity.
|
| 39 |
+
It writes:
|
| 40 |
+
• the top-`budget` quality+diversity designs (+ refolded CIFs),
|
| 41 |
+
• a CSV with all metrics and ranks,
|
| 42 |
+
• a summary PDF with tables, histograms, scatter plots, and (optionally)
|
| 43 |
+
sequence logos and liability heat-maps.
|
| 44 |
+
|
| 45 |
+
The step is designed to be **fast** (≈30 s per 100k designs on typical CPUs);
|
| 46 |
+
the most expensive part is PDF plotting.
|
| 47 |
+
|
| 48 |
+
Parameters
|
| 49 |
+
----------
|
| 50 |
+
design_dir : str | Path
|
| 51 |
+
Directory containing design artifacts and metrics, including:
|
| 52 |
+
- ``aggregate_metrics_*.csv`` (required)
|
| 53 |
+
- ``refold_cif/`` (for copying refolded structures)
|
| 54 |
+
- ``ca_coords_sequences.pkl.gz`` (id→sequence mapping for diversity)
|
| 55 |
+
outdir : str | Path, default=None
|
| 56 |
+
Defaults to design_dir. Parent directory where results will be written. The step creates
|
| 57 |
+
``{outdir}/final_ranked_designs`` and subfolders.
|
| 58 |
+
budget : int, default=30
|
| 59 |
+
Number of designs to select by quality+diversity (Diverse set).
|
| 60 |
+
use_affinity : bool, default=False
|
| 61 |
+
Switch to affinity-oriented metrics (for small-molecule binders).
|
| 62 |
+
from_inverse_folded : bool, default=True
|
| 63 |
+
If True, use backbone RMSDs (``bb_rmsd``) for filtering and delta-SASA from
|
| 64 |
+
refolded structures. If False, use all-atom RMSDs and original ΔSASA.
|
| 65 |
+
filter_designfolding : bool, default=True
|
| 66 |
+
Also require the isolated design (no target) to refold to the same shape,
|
| 67 |
+
via ``designfolding-*_rmsd``.
|
| 68 |
+
filter_bindingsite : bool, default=False
|
| 69 |
+
Keep only designs with at least one residue within the specified distance of
|
| 70 |
+
a binding-site residue (e.g., ``bindsite_under_8rmsd > 0``).
|
| 71 |
+
filter_target_aligned : bool, default=False
|
| 72 |
+
Require target backbone alignment (``bb_target_aligned<2.5`` flag).
|
| 73 |
+
filter_biased : bool, default=True
|
| 74 |
+
Remove amino-acid composition outliers (default caps on ALA/GLY/GLU/LEU/VAL).
|
| 75 |
+
refolding_rmsd_threshold : float, default=2.5
|
| 76 |
+
Threshold used for the RMSD-based filters (lower is better).
|
| 77 |
+
modality : {"peptide","antibody"}, default="peptide"
|
| 78 |
+
Affects liability scoring and optional sequence visualizations.
|
| 79 |
+
alpha : float in [0,1], default=0.1
|
| 80 |
+
Trade-off for sequence diversity selection: 0=quality-only, 1=diversity-only.
|
| 81 |
+
metrics_override : Dict[str, float | None], default=None
|
| 82 |
+
Per-metric *inverse-importance* weights for ranking.
|
| 83 |
+
- A larger value **down-weights** that metric’s rank (rank / weight).
|
| 84 |
+
- ``None`` removes the metric entirely from the ranking key.
|
| 85 |
+
Example: ``{"plip_hbonds_refolded": 4, "delta_sasa_refolded": 2, "neg_min_design_to_target_pae": 1}``.
|
| 86 |
+
num_liability_plots : int, default=0
|
| 87 |
+
If >0, produce per-residue developability heat-maps for the first N top designs.
|
| 88 |
+
plot_seq_logos : bool, default=False
|
| 89 |
+
If True, include alignment logos and AA composition pies for All/Top/Diverse sets.
|
| 90 |
+
additional_filters : list[Dict], default=[]
|
| 91 |
+
Extra hard filters of the form:
|
| 92 |
+
``{"feature": "<column>", "lower_is_better": bool, "threshold": float}``.
|
| 93 |
+
size_buckets : list[Dict], default=[]
|
| 94 |
+
Optional constraint for the maximum number of designs returned in a certain size range:
|
| 95 |
+
``{"num_designs": int, "min": int, "max": int}``.
|
| 96 |
+
|
| 97 |
+
Ranking & Diversity
|
| 98 |
+
-------------------
|
| 99 |
+
**Filtering.** Each design must pass all hard thresholds (``pass_<feature>_filter``).
|
| 100 |
+
The step also adds convenience columns (e.g., ``filter_rmsd``,
|
| 101 |
+
``designfolding-filter_rmsd``, signed variants like ``neg_min_design_to_target_pae``).
|
| 102 |
+
|
| 103 |
+
**Ranking (quality).** For each metric in ``self.metrics``, compute the row's
|
| 104 |
+
rank on the tuple ``(num_filters_passed, metric)`` (so designs that fail filters
|
| 105 |
+
are pushed down), then divide by the metric’s *inverse-importance* weight.
|
| 106 |
+
The **worst** (max) scaled rank across metrics becomes the design’s
|
| 107 |
+
quality key. The Top set is the best `budget` designs by this key (tie-broken
|
| 108 |
+
by iPTM).
|
| 109 |
+
|
| 110 |
+
**Diversity.** A lazy-greedy selection chooses `div_budget` designs maximizing:
|
| 111 |
+
``(1 - alpha) * quality + alpha * (1 - seq_identity)``.
|
| 112 |
+
Sequence identity is computed via pairwise alignment on the full chain or the
|
| 113 |
+
designed segment (auto-chosen based on typical length ratio). Optional
|
| 114 |
+
``size_buckets`` limit the number of selections per length range.
|
| 115 |
+
|
| 116 |
+
Inputs
|
| 117 |
+
------
|
| 118 |
+
• ``design_dir/aggregate_metrics_*.csv`` (required)
|
| 119 |
+
• ``design_dir/ca_coords_sequences.pkl.gz`` (required for diversity)
|
| 120 |
+
• ``design_dir/refold_cif/`` (optional but recommended, copied to outputs)
|
| 121 |
+
|
| 122 |
+
Outputs
|
| 123 |
+
-------
|
| 124 |
+
On disk (under ``{outdir}/final_ranked_designs``):
|
| 125 |
+
• ``final_{budget}_designs`` folder with .mmcif files
|
| 126 |
+
• ``metrics_*.csv`` – full table with ranks/flags
|
| 127 |
+
• ``diverse_selected_{div_budget}.csv`` – rows/IDs for Diverse set
|
| 128 |
+
• ``results_overview_*.pdf`` – summary report with tables/plots
|
| 129 |
+
|
| 130 |
+
Notes
|
| 131 |
+
-----
|
| 132 |
+
- ``metrics_override`` lets you:
|
| 133 |
+
* increase a weight (de-emphasize that metric),
|
| 134 |
+
* set a metric to ``None`` (remove from ranking),
|
| 135 |
+
* include a new metric that is already in the csv for ranking.
|
| 136 |
+
|
| 137 |
+
Examples
|
| 138 |
+
--------
|
| 139 |
+
>>> from boltzgen.task.filter.filter import Filter
|
| 140 |
+
>>> filter = Filter(
|
| 141 |
+
... design_dir="workbench/run123",
|
| 142 |
+
... budget=50, div_budget=30, alpha=0.25,
|
| 143 |
+
... metrics_override={"design_ptm": 2, "neg_min_design_to_target_pae": 1},
|
| 144 |
+
... additional_filters=[{"feature":"design_ptm","lower_is_better":False,"threshold":0.7}],
|
| 145 |
+
... )
|
| 146 |
+
>>> filter.run(jupyter_nb=False) # writes CSVs, PDF, and copies selected structures
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
def __init__(
|
| 150 |
+
self,
|
| 151 |
+
design_dir: str,
|
| 152 |
+
budget: int = 30,
|
| 153 |
+
top_budget: int = 10,
|
| 154 |
+
outdir: str = None,
|
| 155 |
+
use_affinity: bool = False, # This changes the filtering metrics to metrics more amenable to small molecule binder design
|
| 156 |
+
filter_cysteine: bool = True, # This filters out all designs that have designed cysteins in them (prespecified cysteins in the design are not counted)
|
| 157 |
+
from_inverse_folded: bool = True, # This makes it so that we use the backbone refolding rmsd instead of the all-atom RMSD
|
| 158 |
+
filter_designfolding: bool = True, # Additionally filter based on the RMSD from refolding the design in isolation. This makes sure the design has the same shape with and without the target being present.
|
| 159 |
+
filter_bindingsite: bool = False, # This filters out everything that does not have a residue within 4A of a binding site residue
|
| 160 |
+
filter_target_aligned: bool = False,
|
| 161 |
+
filter_biased: bool = True, # This filters out sequences that are alanine rich, 30% alanine is threshold
|
| 162 |
+
refolding_rmsd_threshold: float = 2.5,
|
| 163 |
+
modality: str = "peptide", # peptide, antibody
|
| 164 |
+
peptide_type: str = "linear", # linear, cyclic
|
| 165 |
+
alpha: float = 0.1, # 0 = quality-only, 1 = diversity-only
|
| 166 |
+
random_state: int = 0,
|
| 167 |
+
metrics_override: Dict = None, # overrides metrics, None values delete keys
|
| 168 |
+
num_liability_plots: int = 0,
|
| 169 |
+
plot_seq_logos: bool = False, # make sequence logo diagrams of designed sequence
|
| 170 |
+
additional_filters: list[
|
| 171 |
+
Dict
|
| 172 |
+
] = [], # For example: [{"feature": "design_ALA", "lower_is_better": True, "threshold": 0.3}],
|
| 173 |
+
size_buckets: list[Dict] = [],
|
| 174 |
+
):
|
| 175 |
+
super().__init__()
|
| 176 |
+
assert modality in ["peptide", "antibody"]
|
| 177 |
+
assert peptide_type in ["linear", "cyclic"]
|
| 178 |
+
self.design_dir = Path(design_dir)
|
| 179 |
+
self.top_budget = top_budget
|
| 180 |
+
self.use_affinity = use_affinity
|
| 181 |
+
self.filter_cysteine = filter_cysteine
|
| 182 |
+
self.from_inverse_folded = from_inverse_folded
|
| 183 |
+
self.filter_bindingsite = filter_bindingsite
|
| 184 |
+
self.budget = budget
|
| 185 |
+
self.alpha = alpha
|
| 186 |
+
self.random_state = random_state
|
| 187 |
+
self.num_liability_plots = num_liability_plots
|
| 188 |
+
self.plot_seq_logos = plot_seq_logos
|
| 189 |
+
self.modality = modality
|
| 190 |
+
self.peptide_type = peptide_type
|
| 191 |
+
self.size_buckets = size_buckets
|
| 192 |
+
|
| 193 |
+
if outdir is None:
|
| 194 |
+
outdir = design_dir
|
| 195 |
+
self.outdir = Path(f"{outdir}") / "final_ranked_designs"
|
| 196 |
+
self.top_dir = self.outdir / f"intermediate_ranked_{top_budget}_designs"
|
| 197 |
+
self.div_dir = self.outdir / f"final_{budget}_designs"
|
| 198 |
+
self.outdir.mkdir(parents=True, exist_ok=True)
|
| 199 |
+
self.top_dir.mkdir(parents=True, exist_ok=True)
|
| 200 |
+
self.div_dir.mkdir(parents=True, exist_ok=True)
|
| 201 |
+
|
| 202 |
+
# we want to maximize all these metrics
|
| 203 |
+
self.metrics: dict = {
|
| 204 |
+
"design_to_target_iptm": 1,
|
| 205 |
+
"design_ptm": 1,
|
| 206 |
+
"neg_min_design_to_target_pae": 1,
|
| 207 |
+
"plip_hbonds" + ("_refolded" if from_inverse_folded else ""): 2,
|
| 208 |
+
"plip_saltbridge" + ("_refolded" if from_inverse_folded else ""): 2,
|
| 209 |
+
"delta_sasa_refolded" if from_inverse_folded else "delta_sasa_original": 2,
|
| 210 |
+
}
|
| 211 |
+
if use_affinity:
|
| 212 |
+
self.metrics: dict = {
|
| 213 |
+
"design_to_target_iptm": 1.1,
|
| 214 |
+
"design_ptm": 1.1,
|
| 215 |
+
"neg_min_design_to_target_pae": 1.1,
|
| 216 |
+
"affinity_probability_binary1": 1,
|
| 217 |
+
"plip_hbonds" + ("_refolded" if from_inverse_folded else ""): 2,
|
| 218 |
+
"plip_saltbridge" + ("_refolded" if from_inverse_folded else ""): 2,
|
| 219 |
+
"delta_sasa_refolded"
|
| 220 |
+
if from_inverse_folded
|
| 221 |
+
else "delta_sasa_original": 2,
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
# override metrics
|
| 225 |
+
if not metrics_override is None:
|
| 226 |
+
for k in metrics_override:
|
| 227 |
+
if metrics_override[k] is None:
|
| 228 |
+
self.metrics.pop(k, None)
|
| 229 |
+
else:
|
| 230 |
+
self.metrics[k] = metrics_override[k]
|
| 231 |
+
|
| 232 |
+
# Define how to Filter
|
| 233 |
+
self.filters = [
|
| 234 |
+
{"feature": "has_x", "lower_is_better": True, "threshold": 0},
|
| 235 |
+
{
|
| 236 |
+
"feature": "filter_rmsd",
|
| 237 |
+
"lower_is_better": True,
|
| 238 |
+
"threshold": refolding_rmsd_threshold,
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"feature": "filter_rmsd_design",
|
| 242 |
+
"lower_is_better": True,
|
| 243 |
+
"threshold": refolding_rmsd_threshold,
|
| 244 |
+
},
|
| 245 |
+
]
|
| 246 |
+
if filter_designfolding:
|
| 247 |
+
self.filters.append(
|
| 248 |
+
{
|
| 249 |
+
"feature": "designfolding-filter_rmsd",
|
| 250 |
+
"lower_is_better": True,
|
| 251 |
+
"threshold": refolding_rmsd_threshold,
|
| 252 |
+
}
|
| 253 |
+
)
|
| 254 |
+
if filter_bindingsite:
|
| 255 |
+
self.filters.append(
|
| 256 |
+
{
|
| 257 |
+
"feature": "bindsite_under_8rmsd", # center_coord RMSD
|
| 258 |
+
"lower_is_better": False,
|
| 259 |
+
"threshold": 0.0001, # at least one binding site residue
|
| 260 |
+
},
|
| 261 |
+
)
|
| 262 |
+
if filter_target_aligned:
|
| 263 |
+
self.filters.append(
|
| 264 |
+
{
|
| 265 |
+
"feature": "bb_target_aligned<2.5",
|
| 266 |
+
"lower_is_better": False,
|
| 267 |
+
}
|
| 268 |
+
)
|
| 269 |
+
if filter_cysteine:
|
| 270 |
+
self.filters.append(
|
| 271 |
+
{
|
| 272 |
+
"feature": "CYS_fraction",
|
| 273 |
+
"lower_is_better": True,
|
| 274 |
+
"threshold": 0,
|
| 275 |
+
},
|
| 276 |
+
)
|
| 277 |
+
if filter_biased:
|
| 278 |
+
self.filters.extend(
|
| 279 |
+
[
|
| 280 |
+
{
|
| 281 |
+
"feature": "ALA_fraction",
|
| 282 |
+
"lower_is_better": True,
|
| 283 |
+
"threshold": 0.3,
|
| 284 |
+
},
|
| 285 |
+
{
|
| 286 |
+
"feature": "GLY_fraction",
|
| 287 |
+
"lower_is_better": True,
|
| 288 |
+
"threshold": 0.3,
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"feature": "GLU_fraction",
|
| 292 |
+
"lower_is_better": True,
|
| 293 |
+
"threshold": 0.3,
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"feature": "LEU_fraction",
|
| 297 |
+
"lower_is_better": True,
|
| 298 |
+
"threshold": 0.3,
|
| 299 |
+
},
|
| 300 |
+
{
|
| 301 |
+
"feature": "VAL_fraction",
|
| 302 |
+
"lower_is_better": True,
|
| 303 |
+
"threshold": 0.3,
|
| 304 |
+
},
|
| 305 |
+
]
|
| 306 |
+
)
|
| 307 |
+
self.filters.extend(additional_filters)
|
| 308 |
+
|
| 309 |
+
random.seed(self.random_state)
|
| 310 |
+
np.random.seed(self.random_state)
|
| 311 |
+
|
| 312 |
+
def run(self, config=None, jupyter_nb=False):
|
| 313 |
+
self.load_dataframe()
|
| 314 |
+
self.reset_outdir()
|
| 315 |
+
self.filter_df()
|
| 316 |
+
self.absolute_metrics()
|
| 317 |
+
self.sort_df()
|
| 318 |
+
self.optimize_diversity()
|
| 319 |
+
self.write_outdir()
|
| 320 |
+
|
| 321 |
+
# Visualizations
|
| 322 |
+
print(
|
| 323 |
+
"\nWriting design files is done. Now making plots for a final summary .pdf file with statistics."
|
| 324 |
+
)
|
| 325 |
+
(
|
| 326 |
+
hist_metrics,
|
| 327 |
+
extra_pairs,
|
| 328 |
+
row_headers,
|
| 329 |
+
rows,
|
| 330 |
+
metric_rows,
|
| 331 |
+
intro_text,
|
| 332 |
+
csv_expl_rows,
|
| 333 |
+
) = self.prepare_visualization()
|
| 334 |
+
self.make_visualization(
|
| 335 |
+
hist_metrics,
|
| 336 |
+
extra_pairs,
|
| 337 |
+
row_headers,
|
| 338 |
+
rows,
|
| 339 |
+
metric_rows,
|
| 340 |
+
intro_text,
|
| 341 |
+
csv_expl_rows,
|
| 342 |
+
jupyter_nb=jupyter_nb,
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
def reset_outdir(self):
|
| 346 |
+
if self.outdir.exists():
|
| 347 |
+
shutil.rmtree(self.outdir)
|
| 348 |
+
self.outdir.mkdir(parents=True, exist_ok=True)
|
| 349 |
+
self.top_dir.mkdir(parents=True, exist_ok=True)
|
| 350 |
+
self.div_dir.mkdir(parents=True, exist_ok=True)
|
| 351 |
+
|
| 352 |
+
def load_dataframe(self):
|
| 353 |
+
csv_files = sorted(self.design_dir.glob("aggregate_metrics_*.csv"))
|
| 354 |
+
if not csv_files:
|
| 355 |
+
msg = (
|
| 356 |
+
f"No file starting with 'aggregate_metrics_' found in {self.design_dir}"
|
| 357 |
+
)
|
| 358 |
+
raise FileNotFoundError(msg)
|
| 359 |
+
df_in = pd.read_csv(csv_files[0])
|
| 360 |
+
|
| 361 |
+
self.df_in = df_in.copy()
|
| 362 |
+
df = df_in.copy()
|
| 363 |
+
|
| 364 |
+
if self.from_inverse_folded:
|
| 365 |
+
df["filter_rmsd"] = df["bb_rmsd"]
|
| 366 |
+
df["filter_rmsd_design"] = df["bb_rmsd_design"]
|
| 367 |
+
else:
|
| 368 |
+
df["filter_rmsd"] = df["rmsd"]
|
| 369 |
+
df["filter_rmsd_design"] = df["rmsd_design"]
|
| 370 |
+
if "designfolding-rmsd" in df:
|
| 371 |
+
df["designfolding-filter_rmsd"] = df["designfolding-rmsd"]
|
| 372 |
+
if "designfolding-bb_rmsd" in df and self.from_inverse_folded:
|
| 373 |
+
df["designfolding-filter_rmsd"] = df["designfolding-bb_rmsd"]
|
| 374 |
+
if "min_design_to_target_pae" in df:
|
| 375 |
+
df["neg_min_design_to_target_pae"] = -df["min_design_to_target_pae"]
|
| 376 |
+
|
| 377 |
+
if "design_hydrophobicity" in df:
|
| 378 |
+
df["neg_design_hydrophobicity"] = -df["design_hydrophobicity"]
|
| 379 |
+
if "design_largest_hydrophobic_patch_refolded" in df:
|
| 380 |
+
df["neg_design_largest_hydrophobic_patch_refolded"] = -df[
|
| 381 |
+
"design_largest_hydrophobic_patch_refolded"
|
| 382 |
+
]
|
| 383 |
+
df["neg_min_interaction_pae"] = -df["min_interaction_pae"]
|
| 384 |
+
df["neg_filter_rmsd"] = -df["filter_rmsd"]
|
| 385 |
+
df["neg_filter_rmsd_design"] = -df["filter_rmsd_design"]
|
| 386 |
+
df["has_x"] = df["designed_sequence"].str.contains("X")
|
| 387 |
+
self.df = df
|
| 388 |
+
|
| 389 |
+
print(f"Total number of designs: {len(self.df):>5}")
|
| 390 |
+
before = len(self.df)
|
| 391 |
+
self.df = self.df.drop_duplicates(subset="designed_sequence", keep="first")
|
| 392 |
+
msg = f"Duplicates found: {before - len(self.df)}. Removing duplicates. {len(self.df)} designs remain.\n"
|
| 393 |
+
print(msg)
|
| 394 |
+
|
| 395 |
+
def filter_df(self):
|
| 396 |
+
filter_cols = []
|
| 397 |
+
self.df["num_filters_passed"] = 0
|
| 398 |
+
|
| 399 |
+
for filter in self.filters:
|
| 400 |
+
feat = filter["feature"]
|
| 401 |
+
low = filter["lower_is_better"]
|
| 402 |
+
threshold = filter["threshold"]
|
| 403 |
+
|
| 404 |
+
filter_col = f"pass_{feat}_filter"
|
| 405 |
+
filter_cols.append(filter_col)
|
| 406 |
+
if low:
|
| 407 |
+
self.df[filter_col] = self.df[feat] <= threshold
|
| 408 |
+
else:
|
| 409 |
+
self.df[filter_col] = self.df[feat] >= threshold
|
| 410 |
+
|
| 411 |
+
self.df["num_filters_passed"] += self.df[filter_cols].all(axis=1)
|
| 412 |
+
self.df["pass_filters"] = self.df[filter_cols].all(axis=1)
|
| 413 |
+
|
| 414 |
+
msg = f"Num designs that pass the {feat} filter with threshold {threshold} where {'lower' if low else 'higher'} is better: {self.df[filter_col].sum()}"
|
| 415 |
+
print(msg)
|
| 416 |
+
print(f"Remaining designs: {self.df['pass_filters'].sum()}")
|
| 417 |
+
|
| 418 |
+
num_pass = self.df["pass_filters"].sum()
|
| 419 |
+
if num_pass < self.top_budget or num_pass < self.budget:
|
| 420 |
+
print(
|
| 421 |
+
f"Only {num_pass} designs pass filters. We highly recommend relaxing the thresholds."
|
| 422 |
+
)
|
| 423 |
+
print("\n")
|
| 424 |
+
|
| 425 |
+
def absolute_metrics(self):
|
| 426 |
+
norm_path = Path(__file__).resolve().parents[2] / "resources" / "metrics_normalization.json"
|
| 427 |
+
if not norm_path.exists():
|
| 428 |
+
return
|
| 429 |
+
|
| 430 |
+
with norm_path.open("r") as f:
|
| 431 |
+
norm_stats = json.load(f)
|
| 432 |
+
|
| 433 |
+
for col, stats in norm_stats.items():
|
| 434 |
+
mean = stats["mean"]
|
| 435 |
+
std = stats["std"]
|
| 436 |
+
if col in self.df.columns:
|
| 437 |
+
self.df[col + "_z"] = (self.df[col] - mean) / std
|
| 438 |
+
|
| 439 |
+
importances = {
|
| 440 |
+
"affinity_probability_binary1": 1.5,
|
| 441 |
+
"design_iiptm": 1.0,
|
| 442 |
+
"design_ptm": 0.5,
|
| 443 |
+
"min_design_to_target_pae": -1.0, # lower is better
|
| 444 |
+
"design_hydrophobicity": -0.125, # lower is better
|
| 445 |
+
"design_largest_hydrophobic_patch_refolded": -0.15, # lower is better
|
| 446 |
+
"delta_sasa_refolded": 0.25,
|
| 447 |
+
"plip_saltbridge_refolded": 0.25,
|
| 448 |
+
"plip_hbonds_refolded": 0.25,
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
self.df["absolute_score"] = 0.0
|
| 452 |
+
for base_col, weight in importances.items():
|
| 453 |
+
if base_col in self.df.columns:
|
| 454 |
+
norm_col = base_col + "_z"
|
| 455 |
+
self.df["absolute_score"] += weight * self.df[norm_col]
|
| 456 |
+
total_importance = sum(abs(w) for w in importances.values())
|
| 457 |
+
self.df["absolute_score"] /= total_importance
|
| 458 |
+
|
| 459 |
+
self.df["structure_confidence"] = 0.0
|
| 460 |
+
weight_sum = 0
|
| 461 |
+
for col in ["design_iiptm", "design_ptm", "min_design_to_target_pae"]:
|
| 462 |
+
weight = importances[col]
|
| 463 |
+
norm_col = col + "_z"
|
| 464 |
+
weight_sum += abs(weight)
|
| 465 |
+
self.df["structure_confidence"] += weight * self.df[norm_col]
|
| 466 |
+
self.df["structure_confidence"] /= weight_sum
|
| 467 |
+
|
| 468 |
+
for flt in self.filters:
|
| 469 |
+
feat = flt["feature"]
|
| 470 |
+
filter_col = f"pass_{feat}_filter"
|
| 471 |
+
if "fraction" in feat:
|
| 472 |
+
# If this is a "fraction" feature, meaning a res_type fraction filter, only apply the penalty if num_design > 8
|
| 473 |
+
mask_fail = (self.df["num_design"] > 8) & (self.df[filter_col] == False)
|
| 474 |
+
else:
|
| 475 |
+
mask_fail = self.df[filter_col] == False
|
| 476 |
+
|
| 477 |
+
self.df.loc[mask_fail, "absolute_score"] *= 0.1
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def sort_df(self):
|
| 481 |
+
rank_df = pd.DataFrame(index=self.df.index)
|
| 482 |
+
|
| 483 |
+
# 1. For each row, compute its rank according to each metric
|
| 484 |
+
# Scale the ranks by importance (divide by inverse_importance)
|
| 485 |
+
# Use the feature that we are ranking by AND the number of filters that a design passes. Thus the things that do not pass ass many filters (e.g. they dont pass all the filters) are at the end
|
| 486 |
+
for col, inverse_importance in self.metrics.items():
|
| 487 |
+
if self.metrics[col] is None:
|
| 488 |
+
continue
|
| 489 |
+
|
| 490 |
+
rank_df[f"rank_{col}"] = (
|
| 491 |
+
self.df[["num_filters_passed", col]]
|
| 492 |
+
.apply(tuple, axis=1)
|
| 493 |
+
.rank(method="min", ascending=False)
|
| 494 |
+
.astype(int)
|
| 495 |
+
/ inverse_importance
|
| 496 |
+
)
|
| 497 |
+
self.df = pd.concat([self.df, rank_df], axis=1)
|
| 498 |
+
|
| 499 |
+
# 2. For each row, find the max (worst) rank across the metrics.
|
| 500 |
+
# This single value determines its final rank group.
|
| 501 |
+
self.df["max_rank"] = rank_df.max(axis=1)
|
| 502 |
+
|
| 503 |
+
# 3. Sort by this new max_rank and then create the final dense rank,
|
| 504 |
+
# which is equivalent to the original 'rank_counter'.
|
| 505 |
+
self.df = self.df.sort_values("max_rank")
|
| 506 |
+
self.df["secondary_rank"] = self.df["max_rank"].rank(method="dense").astype(int)
|
| 507 |
+
|
| 508 |
+
# sort by ranking and resolve ties via design_iptm
|
| 509 |
+
self.df = self.df.sort_values(
|
| 510 |
+
by=[
|
| 511 |
+
"secondary_rank",
|
| 512 |
+
"design_to_target_iptm"
|
| 513 |
+
if "design_to_target_iptm" in self.df
|
| 514 |
+
else "design_iptm",
|
| 515 |
+
],
|
| 516 |
+
ascending=[True, False],
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
self.df["final_rank"] = np.arange(1, len(self.df) + 1)
|
| 520 |
+
self.df["quality_score"] = 1 - (self.df["final_rank"] - 1) / (len(self.df) - 1)
|
| 521 |
+
|
| 522 |
+
# Reorder columns
|
| 523 |
+
priority_col_candidates = [
|
| 524 |
+
"id",
|
| 525 |
+
"final_rank",
|
| 526 |
+
"designed_sequence",
|
| 527 |
+
"designed_chain_sequence",
|
| 528 |
+
"num_design",
|
| 529 |
+
"affinity_probability_binary1",
|
| 530 |
+
"design_to_target_iptm"
|
| 531 |
+
if "design_to_target_iptm" in self.df
|
| 532 |
+
else "design_iptm",
|
| 533 |
+
"min_design_to_target_pae"
|
| 534 |
+
if "min_design_to_target_pae" in self.df
|
| 535 |
+
else "min_interaction_pae",
|
| 536 |
+
"design_ptm",
|
| 537 |
+
"filter_rmsd",
|
| 538 |
+
"designfolding-filter_rmsd"
|
| 539 |
+
if "designfolding-filter_rmsd" in self.df
|
| 540 |
+
else "filter_rmsd"
|
| 541 |
+
"plip_saltbridge" + ("_refolded" if self.from_inverse_folded else ""),
|
| 542 |
+
"plip_hbonds" + ("_refolded" if self.from_inverse_folded else ""),
|
| 543 |
+
"delta_sasa_refolded",
|
| 544 |
+
"design_largest_hydrophobic_patch_refolded",
|
| 545 |
+
"design_chain_hydrophobicity",
|
| 546 |
+
"design_hydrophobicity",
|
| 547 |
+
"loop",
|
| 548 |
+
"helix",
|
| 549 |
+
"sheet",
|
| 550 |
+
]
|
| 551 |
+
priority_cols = [c for c in priority_col_candidates if c in self.df.columns]
|
| 552 |
+
|
| 553 |
+
other_cols = [col for col in self.df.columns if col not in priority_cols]
|
| 554 |
+
new_column_order = priority_cols + other_cols
|
| 555 |
+
self.df = self.df[new_column_order]
|
| 556 |
+
|
| 557 |
+
def write_outdir(self):
|
| 558 |
+
num_digits = len(str(len(self.df)))
|
| 559 |
+
|
| 560 |
+
top_dir2 = self.top_dir / "before_refolding"
|
| 561 |
+
top_dir2.mkdir(parents=True, exist_ok=True)
|
| 562 |
+
for i, (idx, row) in tqdm(
|
| 563 |
+
enumerate(self.df[: self.top_budget].iterrows()),
|
| 564 |
+
desc="copy top design files",
|
| 565 |
+
):
|
| 566 |
+
filename = row["file_name"]
|
| 567 |
+
new_filename = f"rank{i:0{num_digits}d}_{filename}"
|
| 568 |
+
src = self.design_dir / filename
|
| 569 |
+
dst = top_dir2 / new_filename
|
| 570 |
+
shutil.copy2(src, dst)
|
| 571 |
+
|
| 572 |
+
src = self.design_dir / "refold_cif" / filename
|
| 573 |
+
dst = self.top_dir / new_filename
|
| 574 |
+
shutil.copy2(src, dst)
|
| 575 |
+
|
| 576 |
+
# save to output/diverse_* directory
|
| 577 |
+
self.div_dir.mkdir(parents=True, exist_ok=True)
|
| 578 |
+
div_dir2 = self.div_dir / "before_refolding"
|
| 579 |
+
div_dir2.mkdir(parents=True, exist_ok=True)
|
| 580 |
+
for i in tqdm(self.diverse_selection, desc="copy diversity files"):
|
| 581 |
+
src = self.design_dir / self.df_m.loc[i, "file_name"]
|
| 582 |
+
qualityrank = self.df_m.loc[i, "final_rank"]
|
| 583 |
+
filename = src.name
|
| 584 |
+
new_filename = f"rank{qualityrank:0{num_digits}d}_{filename}"
|
| 585 |
+
shutil.copy2(src, div_dir2 / new_filename)
|
| 586 |
+
|
| 587 |
+
src = self.design_dir / "refold_cif" / self.df_m.loc[i, "file_name"]
|
| 588 |
+
shutil.copy2(src, self.div_dir / new_filename)
|
| 589 |
+
self.df_div.to_csv(
|
| 590 |
+
self.outdir / f"final_designs_metrics_{self.budget}.csv", index=False
|
| 591 |
+
)
|
| 592 |
+
print("Files + CSV saved to", self.outdir)
|
| 593 |
+
|
| 594 |
+
self.df.to_csv(self.outdir / f"all_designs_metrics.csv", index=False)
|
| 595 |
+
|
| 596 |
+
def optimize_diversity(self):
|
| 597 |
+
# Load structures and sequences to compute similarities
|
| 598 |
+
seq_path = self.design_dir / "ca_coords_sequences.pkl.gz"
|
| 599 |
+
if not seq_path.exists():
|
| 600 |
+
raise FileNotFoundError(f"Expected {seq_path} to exist")
|
| 601 |
+
df_seq = pd.read_pickle(seq_path)[["id", "sequence"]]
|
| 602 |
+
|
| 603 |
+
self.df_m = pd.merge(self.df, df_seq, on="id", how="inner").reset_index(
|
| 604 |
+
drop=True
|
| 605 |
+
)
|
| 606 |
+
seqs = self.df_m["sequence"].tolist()
|
| 607 |
+
quality = self.df_m["quality_score"].to_numpy()
|
| 608 |
+
|
| 609 |
+
# sequence-only similarity
|
| 610 |
+
aligner = Align.PairwiseAligner()
|
| 611 |
+
pid_cache = {}
|
| 612 |
+
|
| 613 |
+
def sim_fn(i, j):
|
| 614 |
+
if i == j:
|
| 615 |
+
return 1.0
|
| 616 |
+
key = tuple(sorted((i, j)))
|
| 617 |
+
if key not in pid_cache:
|
| 618 |
+
seq1 = seqs[i]
|
| 619 |
+
seq2 = seqs[j]
|
| 620 |
+
aln = aligner.align(seq1, seq2)[0]
|
| 621 |
+
pid_cache[key] = aln.score / max(len(seqs[i]), len(seqs[j]))
|
| 622 |
+
return pid_cache[key]
|
| 623 |
+
|
| 624 |
+
random.seed(self.random_state)
|
| 625 |
+
np.random.seed(self.random_state)
|
| 626 |
+
diverse_selection = self.select_lazy_greedy(self.budget, quality, sim_fn)
|
| 627 |
+
|
| 628 |
+
self.diverse_selection = diverse_selection
|
| 629 |
+
self.df_div = self.df_m.iloc[diverse_selection].reset_index(drop=True)
|
| 630 |
+
|
| 631 |
+
def select_lazy_greedy(self, k, quality, sim_fn):
|
| 632 |
+
# Handle edge case where we have fewer items than requested
|
| 633 |
+
if len(quality) <= k:
|
| 634 |
+
return list(range(len(quality)))
|
| 635 |
+
|
| 636 |
+
selected = [int(np.argmax(quality))]
|
| 637 |
+
remaining = set(range(len(quality))) - set(selected)
|
| 638 |
+
|
| 639 |
+
heap = []
|
| 640 |
+
for i in remaining:
|
| 641 |
+
div = 1 - sim_fn(i, selected[0])
|
| 642 |
+
gain = (1 - self.alpha) * quality[i] + self.alpha * div
|
| 643 |
+
heapq.heappush(heap, (-gain, i))
|
| 644 |
+
|
| 645 |
+
buckets = np.zeros(len(self.size_buckets) + 1)
|
| 646 |
+
first = selected[0]
|
| 647 |
+
first_len = len(self.df_m["sequence"][first])
|
| 648 |
+
for idx, bucket_size in enumerate(self.size_buckets):
|
| 649 |
+
if first_len >= bucket_size["min"] and first_len < bucket_size["max"]:
|
| 650 |
+
buckets[idx] += 1
|
| 651 |
+
break
|
| 652 |
+
for _ in tqdm(
|
| 653 |
+
range(k - 1), desc="Performing lazy greedy diversity optimization."
|
| 654 |
+
):
|
| 655 |
+
while True:
|
| 656 |
+
neg_gain, cand = heapq.heappop(heap)
|
| 657 |
+
num_design = len(self.df_m["sequence"][cand])
|
| 658 |
+
bucket_idx = None
|
| 659 |
+
for idx, bucket_size in enumerate(self.size_buckets):
|
| 660 |
+
if (
|
| 661 |
+
num_design < bucket_size["max"]
|
| 662 |
+
and num_design >= bucket_size["min"]
|
| 663 |
+
):
|
| 664 |
+
bucket_idx = idx
|
| 665 |
+
|
| 666 |
+
if bucket_idx is None:
|
| 667 |
+
bucket_idx = len(self.size_buckets)
|
| 668 |
+
|
| 669 |
+
bucket_full = (
|
| 670 |
+
not bucket_idx == len(self.size_buckets)
|
| 671 |
+
and buckets[bucket_idx]
|
| 672 |
+
== self.size_buckets[bucket_idx]["num_designs"]
|
| 673 |
+
)
|
| 674 |
+
if bucket_full:
|
| 675 |
+
continue
|
| 676 |
+
|
| 677 |
+
true_div = 1 - max(sim_fn(cand, j) for j in selected)
|
| 678 |
+
true_g = (1 - self.alpha) * quality[cand] + self.alpha * true_div
|
| 679 |
+
heapq.heappush(heap, (-true_g, cand))
|
| 680 |
+
|
| 681 |
+
if heap[0][1] == cand:
|
| 682 |
+
heapq.heappop(heap)
|
| 683 |
+
selected.append(cand)
|
| 684 |
+
remaining.remove(cand)
|
| 685 |
+
buckets[bucket_idx] += 1
|
| 686 |
+
break
|
| 687 |
+
return sorted(selected)
|
| 688 |
+
|
| 689 |
+
def prepare_visualization(self):
|
| 690 |
+
summary_metrics = [
|
| 691 |
+
"num_design",
|
| 692 |
+
"filter_rmsd",
|
| 693 |
+
"designfolding-filter_rmsd"
|
| 694 |
+
if "designfolding-filter_rmsd" in self.df
|
| 695 |
+
else "filter_rmsddesign_ptm",
|
| 696 |
+
"design_iptm",
|
| 697 |
+
"design_to_target_iptm"
|
| 698 |
+
if "design_to_target_iptm" in self.df
|
| 699 |
+
else "design_iptm",
|
| 700 |
+
"min_design_to_target_pae"
|
| 701 |
+
if "min_design_to_target_pae" in self.df
|
| 702 |
+
else "min_interaction_pae",
|
| 703 |
+
"design_ipsae_min"
|
| 704 |
+
if "design_ipsae_min" in self.df
|
| 705 |
+
else "design_iptm",
|
| 706 |
+
"design_to_target_ipsae"
|
| 707 |
+
if "design_to_target_ipsae" in self.df
|
| 708 |
+
else "design_iptm",
|
| 709 |
+
"delta_sasa_refolded"
|
| 710 |
+
if self.from_inverse_folded
|
| 711 |
+
else "delta_sasa_original",
|
| 712 |
+
"plip_saltbridge" + ("_refolded" if self.from_inverse_folded else ""),
|
| 713 |
+
"plip_hbonds" + ("_refolded" if self.from_inverse_folded else ""),
|
| 714 |
+
]
|
| 715 |
+
|
| 716 |
+
# Scatter pairs (each will be one page)
|
| 717 |
+
extra_pairs = [
|
| 718 |
+
("num_design", "rank"),
|
| 719 |
+
(
|
| 720 |
+
"num_design",
|
| 721 |
+
"plip_saltbridge" + ("_refolded" if self.from_inverse_folded else ""),
|
| 722 |
+
),
|
| 723 |
+
(
|
| 724 |
+
"num_design",
|
| 725 |
+
"plip_hbonds" + ("_refolded" if self.from_inverse_folded else ""),
|
| 726 |
+
),
|
| 727 |
+
(
|
| 728 |
+
"num_design",
|
| 729 |
+
"delta_sasa" + ("_refolded" if self.from_inverse_folded else ""),
|
| 730 |
+
),
|
| 731 |
+
(
|
| 732 |
+
"num_design",
|
| 733 |
+
"design_ptm",
|
| 734 |
+
),
|
| 735 |
+
(
|
| 736 |
+
"num_design",
|
| 737 |
+
"min_design_to_target_pae"
|
| 738 |
+
if "min_design_to_target_pae" in self.df
|
| 739 |
+
else "min_interaction_pae",
|
| 740 |
+
),
|
| 741 |
+
(
|
| 742 |
+
"num_design",
|
| 743 |
+
"design_to_target_iptm"
|
| 744 |
+
if "design_to_target_iptm" in self.df
|
| 745 |
+
else "design_iptm",
|
| 746 |
+
),
|
| 747 |
+
(
|
| 748 |
+
"num_design",
|
| 749 |
+
"design_iptm",
|
| 750 |
+
),
|
| 751 |
+
(
|
| 752 |
+
"num_design",
|
| 753 |
+
"design_ipsae_min" if "design_ipsae_min" in self.df else "design_iptm",
|
| 754 |
+
),
|
| 755 |
+
(
|
| 756 |
+
"num_design",
|
| 757 |
+
"design_to_target_ipsae" if "design_to_target_ipsae" in self.df else "design_iptm",
|
| 758 |
+
),
|
| 759 |
+
(
|
| 760 |
+
"num_design",
|
| 761 |
+
"design_iiptm" if "design_iiptm" in self.df else "design_iptm",
|
| 762 |
+
),
|
| 763 |
+
(
|
| 764 |
+
"num_design",
|
| 765 |
+
"design_largest_hydrophobic_patch_refolded"
|
| 766 |
+
if "design_largest_hydrophobic_patch_refolded" in self.df
|
| 767 |
+
else "design_iptm",
|
| 768 |
+
),
|
| 769 |
+
(
|
| 770 |
+
"num_design",
|
| 771 |
+
"design_hydrophobicity"
|
| 772 |
+
if "design_hydrophobicity" in self.df
|
| 773 |
+
else "design_iptm",
|
| 774 |
+
),
|
| 775 |
+
]
|
| 776 |
+
if not self.from_inverse_folded:
|
| 777 |
+
extra_pairs.append(("delta_sasa_refolded", "delta_sasa_original"))
|
| 778 |
+
extra_pairs.append(("plip_saltbridge", "delta_sasa_original"))
|
| 779 |
+
|
| 780 |
+
# Histograms with selected overlay
|
| 781 |
+
hist_metrics = [
|
| 782 |
+
"num_design",
|
| 783 |
+
"filter_rmsd",
|
| 784 |
+
"designfolding-filter_rmsd"
|
| 785 |
+
if "designfolding-filter_rmsd" in self.df
|
| 786 |
+
else "filter_rmsddesign_ptm",
|
| 787 |
+
"design_to_target_iptm"
|
| 788 |
+
if "design_to_target_iptm" in self.df
|
| 789 |
+
else "design_iptm",
|
| 790 |
+
"design_iptm",
|
| 791 |
+
"design_ipsae_min"
|
| 792 |
+
if "design_ipsae_min" in self.df
|
| 793 |
+
else "design_iptm",
|
| 794 |
+
"design_to_target_ipsae"
|
| 795 |
+
if "design_to_target_ipsae" in self.df
|
| 796 |
+
else "design_iptm",
|
| 797 |
+
"min_design_to_target_pae"
|
| 798 |
+
if "min_design_to_target_pae" in self.df
|
| 799 |
+
else "min_interaction_pae",
|
| 800 |
+
"plip_saltbridge" + ("_refolded" if self.from_inverse_folded else ""),
|
| 801 |
+
"plip_hbonds" + ("_refolded" if self.from_inverse_folded else ""),
|
| 802 |
+
"plip_hydrophobic" + ("_refolded" if self.from_inverse_folded else ""),
|
| 803 |
+
"delta_sasa_refolded",
|
| 804 |
+
"design_largest_hydrophobic_patch_refolded"
|
| 805 |
+
if "design_largest_hydrophobic_patch_refolded" in self.df
|
| 806 |
+
else "delta_sasa_refolded",
|
| 807 |
+
"design_hydrophobicity"
|
| 808 |
+
if "design_hydrophobicity" in self.df
|
| 809 |
+
else "delta_sasa_refolded",
|
| 810 |
+
]
|
| 811 |
+
if not self.from_inverse_folded:
|
| 812 |
+
hist_metrics.append("delta_sasa_original")
|
| 813 |
+
|
| 814 |
+
hist_metrics = list(dict.fromkeys(hist_metrics))
|
| 815 |
+
extra_pairs = list(dict.fromkeys(extra_pairs))
|
| 816 |
+
|
| 817 |
+
# Prepend any active ranking metrics not already in the lists
|
| 818 |
+
extra_ranking = [m for m in self.metrics if m in self.df.columns and m not in hist_metrics]
|
| 819 |
+
hist_metrics = extra_ranking + hist_metrics
|
| 820 |
+
summary_metrics = extra_ranking + summary_metrics
|
| 821 |
+
extra_pairs = [("num_design", m) for m in extra_ranking] + extra_pairs
|
| 822 |
+
|
| 823 |
+
if self.use_affinity:
|
| 824 |
+
summary_metrics.insert(2, "affinity_probability_binary1")
|
| 825 |
+
hist_metrics.insert(2, "affinity_probability_binary1")
|
| 826 |
+
|
| 827 |
+
avail = [m for m in summary_metrics if m in self.df.columns]
|
| 828 |
+
base_rows = [
|
| 829 |
+
["Num designs", len(self.df), "-"],
|
| 830 |
+
]
|
| 831 |
+
|
| 832 |
+
extra_mean = (
|
| 833 |
+
[
|
| 834 |
+
m,
|
| 835 |
+
f"{self.df[m].mean():.3f}", # mean of ALL
|
| 836 |
+
f"{self.df[: self.top_budget][m].mean():.3f}", # mean of red set
|
| 837 |
+
f"{self.df_div[m].mean():.3f}", # mean of BLUE set
|
| 838 |
+
]
|
| 839 |
+
for m in avail
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
for row in base_rows:
|
| 843 |
+
row.append("-")
|
| 844 |
+
|
| 845 |
+
rows = base_rows + list(extra_mean)
|
| 846 |
+
|
| 847 |
+
row_headers = [
|
| 848 |
+
"Metric",
|
| 849 |
+
f"Mean",
|
| 850 |
+
f"Mean top {self.top_budget}",
|
| 851 |
+
f"Mean top {self.budget} diverse",
|
| 852 |
+
]
|
| 853 |
+
|
| 854 |
+
metric_rows = [[k, v] for k, v in self.metrics.items()]
|
| 855 |
+
|
| 856 |
+
n_total = len(self.df)
|
| 857 |
+
n_pass_filters = (
|
| 858 |
+
int(self.df["pass_filters"].sum()) if "pass_filters" in self.df else n_total
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
+
text = f"""
|
| 862 |
+
• Designs generated: {n_total}
|
| 863 |
+
• Designs passing all filters: {n_pass_filters}
|
| 864 |
+
|
| 865 |
+
• MMCIF files of final designs are in:
|
| 866 |
+
{self.div_dir}
|
| 867 |
+
|
| 868 |
+
• Metrics and sequences of {self.budget} final designs are in:
|
| 869 |
+
{self.outdir}/final_designs_metrics_{self.budget}.csv
|
| 870 |
+
|
| 871 |
+
• Metrics of all designs are in:
|
| 872 |
+
{self.outdir}/all_designs_metrics.csv
|
| 873 |
+
|
| 874 |
+
You can rerun filtering (very quick), using this command with changed parameters:
|
| 875 |
+
-- boltzgen run input_spec.yaml --steps filtering --config filtering budget=60 alpha=0.05
|
| 876 |
+
You can also rerun filtering in a jupyter notebook if you want using `filter.ipynb`
|
| 877 |
+
|
| 878 |
+
What was run to produce this in the Filter task:
|
| 879 |
+
1. Filtering: each design is evaluated against mandatory thresholds.
|
| 880 |
+
2. Ranking: for every metric we compute its rank, then scale it by the metric’s inverse-importance weight. Designs with fewer passed filters are automatically penalised because the ranking key is the pair (num_filters_passed, metric). The overall quality score is the worst (maximum) of these scaled ranks. The {self.top_budget} best designs form the Top set.
|
| 881 |
+
3. Diversity: a lazy-greedy algorithm selects {self.budget} designs that jointly maximise quality and
|
| 882 |
+
minimise sequence similarity (sequence-identity distance). The trade-off is controlled by α = {self.alpha}:
|
| 883 |
+
• α = 0 → 100 % quality focus (same as Top set)
|
| 884 |
+
• α = 1 → 100 % diversity focus (ignores quality)
|
| 885 |
+
• Quality – composite of metrics such as iPTM, salt-bridges, ΔSASA, etc. Each metric has an "inverse importance" weight (see "Sorting Criteria" table). A larger weight divides the rank by a bigger number and therefore down-weights that metric.
|
| 886 |
+
• Diversity – 1 − sequence identity between designs.
|
| 887 |
+
We use α = {self.alpha}, meaning {round((1 - self.alpha) * 100)} % emphasis on quality, {round(self.alpha * 100)} % on diversity.
|
| 888 |
+
|
| 889 |
+
"""
|
| 890 |
+
|
| 891 |
+
csv_expl_rows = [
|
| 892 |
+
["id", "filename to retrieve the file"],
|
| 893 |
+
["design_sequence", "designed amino acids (may be subset of chain)"],
|
| 894 |
+
[
|
| 895 |
+
"designed_chain_sequence",
|
| 896 |
+
"full sequence of the chain containing designed residues (recommended for synthesis)",
|
| 897 |
+
],
|
| 898 |
+
["num_design", "number of designed residues"],
|
| 899 |
+
["secondary_rank", "intermediate rank from the sorting procedure"],
|
| 900 |
+
[
|
| 901 |
+
"design_ptm",
|
| 902 |
+
"predicted TM score for intra-design contacts (higher = better)",
|
| 903 |
+
],
|
| 904 |
+
[
|
| 905 |
+
"design_iptm",
|
| 906 |
+
"predicted TM score for design–target contacts (higher = better)",
|
| 907 |
+
],
|
| 908 |
+
[
|
| 909 |
+
"design_to_target_iptm",
|
| 910 |
+
"same as design_iptm but for multi-chain designs",
|
| 911 |
+
],
|
| 912 |
+
[
|
| 913 |
+
"design_ipsae_min",
|
| 914 |
+
"min interaction pSAE: PAE-based confidence score for interaction (higher = better)",
|
| 915 |
+
],
|
| 916 |
+
[
|
| 917 |
+
"design_to_target_ipsae",
|
| 918 |
+
"interaction pSAE between design and target (higher = better)",
|
| 919 |
+
],
|
| 920 |
+
[
|
| 921 |
+
"min_design_to_target_pae",
|
| 922 |
+
"minimum PAE between design & target (lower = better)",
|
| 923 |
+
],
|
| 924 |
+
["plip_saltbridge", "number of salt-bridge interactions"],
|
| 925 |
+
["plip_hbonds", "number of hydrogen-bond interactions"],
|
| 926 |
+
["plip_hydrophobic", "number of hydrophobic interactions"],
|
| 927 |
+
["delta_sasa_original", "ΔSASA when binder present vs absent"],
|
| 928 |
+
["delta_sasa_refolded", "same as above but on the refolded structure"],
|
| 929 |
+
]
|
| 930 |
+
|
| 931 |
+
intro_text = text
|
| 932 |
+
|
| 933 |
+
return (
|
| 934 |
+
hist_metrics,
|
| 935 |
+
extra_pairs,
|
| 936 |
+
row_headers,
|
| 937 |
+
rows,
|
| 938 |
+
metric_rows,
|
| 939 |
+
intro_text,
|
| 940 |
+
csv_expl_rows,
|
| 941 |
+
)
|
| 942 |
+
|
| 943 |
+
def make_visualization(
|
| 944 |
+
self,
|
| 945 |
+
hist_metrics,
|
| 946 |
+
extra_pairs,
|
| 947 |
+
row_headers,
|
| 948 |
+
rows,
|
| 949 |
+
metric_rows,
|
| 950 |
+
intro_text,
|
| 951 |
+
csv_expl_rows,
|
| 952 |
+
jupyter_nb=False,
|
| 953 |
+
):
|
| 954 |
+
pdf_path = self.outdir / f"results_overview.pdf"
|
| 955 |
+
pdf = PdfPages(pdf_path)
|
| 956 |
+
|
| 957 |
+
def _ensure_width(fig, target_w=8.5):
|
| 958 |
+
w, h = fig.get_size_inches()
|
| 959 |
+
if abs(w - target_w) > 0.01:
|
| 960 |
+
scale = target_w / w
|
| 961 |
+
fig.set_size_inches(target_w, h * scale, forward=True)
|
| 962 |
+
|
| 963 |
+
def show(fig):
|
| 964 |
+
_ensure_width(fig)
|
| 965 |
+
plt.tight_layout()
|
| 966 |
+
pdf.savefig(fig)
|
| 967 |
+
if jupyter_nb:
|
| 968 |
+
plt.show(fig)
|
| 969 |
+
plt.close(fig)
|
| 970 |
+
|
| 971 |
+
def _wrap(txt: str, width: int = 60):
|
| 972 |
+
return "\n".join(fill(line, width) for line in txt.splitlines())
|
| 973 |
+
|
| 974 |
+
def _format_body(txt: str, width: int | None = 95):
|
| 975 |
+
txt = dedent(txt).strip("\n")
|
| 976 |
+
if width is None:
|
| 977 |
+
return txt
|
| 978 |
+
out_lines = []
|
| 979 |
+
for line in txt.splitlines():
|
| 980 |
+
if not line.strip():
|
| 981 |
+
out_lines.append("")
|
| 982 |
+
continue
|
| 983 |
+
m = re.match(r"^(\s*)([•\d]+[.)]?)(\s+)(.*)", line)
|
| 984 |
+
if m:
|
| 985 |
+
lead_ws, bullet, spacer, rest = m.groups()
|
| 986 |
+
bullet_str = f"{lead_ws}{bullet}{spacer}"
|
| 987 |
+
wrapped = fill(
|
| 988 |
+
rest,
|
| 989 |
+
width=max(20, width - len(bullet_str)),
|
| 990 |
+
subsequent_indent=" " * len(bullet_str),
|
| 991 |
+
)
|
| 992 |
+
wrapped_lines = wrapped.split("\n")
|
| 993 |
+
wrapped_lines[0] = bullet_str + wrapped_lines[0]
|
| 994 |
+
out_lines.extend(wrapped_lines)
|
| 995 |
+
else:
|
| 996 |
+
out_lines.extend(fill(line, width).split("\n"))
|
| 997 |
+
return "\n".join(out_lines)
|
| 998 |
+
|
| 999 |
+
def section_page(title: str, body: str = ""):
|
| 1000 |
+
is_overview = title.startswith("Results Overview")
|
| 1001 |
+
sec_height = 11 if is_overview else 4.5
|
| 1002 |
+
fig, ax = plt.subplots(figsize=(8.5, sec_height))
|
| 1003 |
+
ax.axis("off")
|
| 1004 |
+
main, sub = (
|
| 1005 |
+
(title.split("–", 1) + [None])[:2] if "–" in title else (title, None)
|
| 1006 |
+
)
|
| 1007 |
+
base_fs = 20
|
| 1008 |
+
fs_main = max(14, base_fs - int(len(main) / 40) * 2)
|
| 1009 |
+
fs_sub = fs_main - 2
|
| 1010 |
+
ax.text(
|
| 1011 |
+
0.6 if is_overview else 0.5,
|
| 1012 |
+
0.9 if is_overview else 0.75,
|
| 1013 |
+
_wrap(main.strip(), 40),
|
| 1014 |
+
fontsize=fs_main,
|
| 1015 |
+
ha="center",
|
| 1016 |
+
va="top",
|
| 1017 |
+
weight="bold",
|
| 1018 |
+
)
|
| 1019 |
+
if sub:
|
| 1020 |
+
ax.text(
|
| 1021 |
+
0.5,
|
| 1022 |
+
0.66,
|
| 1023 |
+
_wrap(sub.strip(), 50),
|
| 1024 |
+
fontsize=fs_sub,
|
| 1025 |
+
ha="center",
|
| 1026 |
+
va="top",
|
| 1027 |
+
style="italic",
|
| 1028 |
+
)
|
| 1029 |
+
|
| 1030 |
+
def _render_line(text, y, is_path=False):
|
| 1031 |
+
ax.text(
|
| 1032 |
+
0.12,
|
| 1033 |
+
y,
|
| 1034 |
+
text,
|
| 1035 |
+
fontsize=10,
|
| 1036 |
+
ha="left",
|
| 1037 |
+
va="top",
|
| 1038 |
+
wrap=False,
|
| 1039 |
+
fontfamily="monospace" if is_path else None,
|
| 1040 |
+
color="blue" if is_path else "black",
|
| 1041 |
+
)
|
| 1042 |
+
|
| 1043 |
+
if body:
|
| 1044 |
+
if is_overview:
|
| 1045 |
+
y_pos = 0.78
|
| 1046 |
+
for para in re.split(r"\n\s*\n", body.strip()):
|
| 1047 |
+
lines = para.strip().split("\n")
|
| 1048 |
+
header = lines[0].strip()
|
| 1049 |
+
rest = (
|
| 1050 |
+
"\n".join(l.strip() for l in lines[1:])
|
| 1051 |
+
if len(lines) > 1
|
| 1052 |
+
else ""
|
| 1053 |
+
)
|
| 1054 |
+
ax.text(
|
| 1055 |
+
0.12,
|
| 1056 |
+
y_pos,
|
| 1057 |
+
header,
|
| 1058 |
+
fontsize=12,
|
| 1059 |
+
fontweight="bold",
|
| 1060 |
+
ha="left",
|
| 1061 |
+
va="top",
|
| 1062 |
+
wrap=True,
|
| 1063 |
+
)
|
| 1064 |
+
y_pos -= 0.05
|
| 1065 |
+
|
| 1066 |
+
if rest:
|
| 1067 |
+
for line in _format_body(rest, width=100).split("\n"):
|
| 1068 |
+
_render_line(
|
| 1069 |
+
line, y_pos, is_path=line.lstrip().startswith("--")
|
| 1070 |
+
)
|
| 1071 |
+
y_pos -= 0.03
|
| 1072 |
+
y_pos -= 0.05
|
| 1073 |
+
else:
|
| 1074 |
+
ax.text(
|
| 1075 |
+
0.12,
|
| 1076 |
+
0.46,
|
| 1077 |
+
_format_body(body, width=90),
|
| 1078 |
+
fontsize=10,
|
| 1079 |
+
ha="left",
|
| 1080 |
+
va="top",
|
| 1081 |
+
wrap=True,
|
| 1082 |
+
)
|
| 1083 |
+
show(fig)
|
| 1084 |
+
|
| 1085 |
+
section_page("Results Overview", body=_format_body(intro_text, width=110))
|
| 1086 |
+
|
| 1087 |
+
csv_expl_rows_wrapped = [[col, _wrap(desc, 100)] for col, desc in csv_expl_rows]
|
| 1088 |
+
section_page("CSV Column Reference")
|
| 1089 |
+
fig_height = 0.35 * len(csv_expl_rows_wrapped) + 1.5
|
| 1090 |
+
fig, ax = plt.subplots(figsize=(8.5, fig_height))
|
| 1091 |
+
ax.axis("off")
|
| 1092 |
+
table = ax.table(
|
| 1093 |
+
cellText=csv_expl_rows_wrapped,
|
| 1094 |
+
colLabels=["Column", "Description"],
|
| 1095 |
+
colLoc="left",
|
| 1096 |
+
colWidths=[0.25, 0.70],
|
| 1097 |
+
loc="center",
|
| 1098 |
+
)
|
| 1099 |
+
table.auto_set_font_size(False)
|
| 1100 |
+
table.set_fontsize(7)
|
| 1101 |
+
table.scale(1, 1.2)
|
| 1102 |
+
show(fig)
|
| 1103 |
+
|
| 1104 |
+
section_page(
|
| 1105 |
+
"Filtering & Sorting Criteria",
|
| 1106 |
+
body=(
|
| 1107 |
+
"This section first lists threshold filters that every design must pass (Filtering Criteria table)"
|
| 1108 |
+
"then explains how designs are ranked (Sorting Criteria table) weighted by their inverse importance."
|
| 1109 |
+
),
|
| 1110 |
+
)
|
| 1111 |
+
|
| 1112 |
+
filters_df = pd.DataFrame(self.filters)
|
| 1113 |
+
filters_df["Pass"] = 0
|
| 1114 |
+
for i, filter in enumerate(self.filters):
|
| 1115 |
+
filters_df.at[i, "Pass"] = self.df[f"pass_{filter['feature']}_filter"].sum()
|
| 1116 |
+
|
| 1117 |
+
fig_height = 0.4 * len(filters_df) + 2
|
| 1118 |
+
fig, ax = plt.subplots(figsize=(8.5, fig_height))
|
| 1119 |
+
ax.axis("off")
|
| 1120 |
+
ax.text(
|
| 1121 |
+
0.5,
|
| 1122 |
+
1.0,
|
| 1123 |
+
"Filtering Criteria",
|
| 1124 |
+
fontsize=14,
|
| 1125 |
+
ha="center",
|
| 1126 |
+
va="bottom",
|
| 1127 |
+
transform=ax.transAxes,
|
| 1128 |
+
)
|
| 1129 |
+
table = ax.table(
|
| 1130 |
+
cellText=filters_df.values,
|
| 1131 |
+
colLabels=filters_df.columns.tolist(),
|
| 1132 |
+
loc="center",
|
| 1133 |
+
)
|
| 1134 |
+
table.auto_set_font_size(False)
|
| 1135 |
+
table.set_fontsize(8)
|
| 1136 |
+
table.scale(1, 1.2)
|
| 1137 |
+
show(fig)
|
| 1138 |
+
fig_height = 0.4 * len(metric_rows) + 1
|
| 1139 |
+
fig, ax = plt.subplots(figsize=(8.5, fig_height))
|
| 1140 |
+
ax.axis("off")
|
| 1141 |
+
ax.text(
|
| 1142 |
+
0.5,
|
| 1143 |
+
1.0,
|
| 1144 |
+
"Sorting Criteria",
|
| 1145 |
+
fontsize=14,
|
| 1146 |
+
ha="center",
|
| 1147 |
+
va="bottom",
|
| 1148 |
+
transform=ax.transAxes,
|
| 1149 |
+
)
|
| 1150 |
+
table = ax.table(
|
| 1151 |
+
cellText=metric_rows,
|
| 1152 |
+
colLabels=["Metric", "Inverse Importance"],
|
| 1153 |
+
loc="center",
|
| 1154 |
+
)
|
| 1155 |
+
table.auto_set_font_size(False)
|
| 1156 |
+
table.set_fontsize(8)
|
| 1157 |
+
table.scale(1, 1.2)
|
| 1158 |
+
show(fig)
|
| 1159 |
+
|
| 1160 |
+
section_page(
|
| 1161 |
+
"Results Summary – Aggregate statistics",
|
| 1162 |
+
body=(
|
| 1163 |
+
f"Quick numeric overview comparing (i) ALL incoming designs, (ii) the top-{self.top_budget} highest-quality "
|
| 1164 |
+
f"designs, and (iii) the {self.budget} quality+diversity designs produced by the lazy-greedy "
|
| 1165 |
+
"selection."
|
| 1166 |
+
),
|
| 1167 |
+
)
|
| 1168 |
+
if rows:
|
| 1169 |
+
fig, ax = plt.subplots(figsize=(8.5, 0.4 * len(rows) + 1))
|
| 1170 |
+
ax.axis("off")
|
| 1171 |
+
table = ax.table(
|
| 1172 |
+
cellText=rows,
|
| 1173 |
+
colLabels=row_headers,
|
| 1174 |
+
loc="center",
|
| 1175 |
+
)
|
| 1176 |
+
table.auto_set_font_size(False)
|
| 1177 |
+
table.set_fontsize(8)
|
| 1178 |
+
table.scale(1, 1.2)
|
| 1179 |
+
show(fig)
|
| 1180 |
+
|
| 1181 |
+
if self.plot_seq_logos:
|
| 1182 |
+
section_page(
|
| 1183 |
+
"Sequence Logos & AA Composition – Motifs & biases",
|
| 1184 |
+
body=(
|
| 1185 |
+
"Sequence logos display the per-position amino-acid preferences after multiple-sequence alignment, "
|
| 1186 |
+
"highlighting motifs that emerge in the design sets. The accompanying pies summarise overall "
|
| 1187 |
+
"hydrophobicity and charge composition comparing ALL, Top-quality (red), and Diversity-optimised subsets. "
|
| 1188 |
+
),
|
| 1189 |
+
)
|
| 1190 |
+
vis = (
|
| 1191 |
+
"designed_sequence"
|
| 1192 |
+
if (
|
| 1193 |
+
self.df["designed_chain_sequence"].str.len().mean()
|
| 1194 |
+
> 1.5 * self.df["designed_sequence"].str.len().mean()
|
| 1195 |
+
)
|
| 1196 |
+
else "designed_chain_sequence"
|
| 1197 |
+
)
|
| 1198 |
+
seq_sets = [
|
| 1199 |
+
(
|
| 1200 |
+
f"All {len(self.df)} {vis}",
|
| 1201 |
+
self.df[vis].tolist(),
|
| 1202 |
+
),
|
| 1203 |
+
(
|
| 1204 |
+
f"Top {self.top_budget} {vis}",
|
| 1205 |
+
self.df[vis].tolist()[: self.top_budget],
|
| 1206 |
+
),
|
| 1207 |
+
(
|
| 1208 |
+
f"Diverse {self.budget} {vis}",
|
| 1209 |
+
self.df_div[vis].tolist(),
|
| 1210 |
+
),
|
| 1211 |
+
]
|
| 1212 |
+
if self.plot_seq_logos:
|
| 1213 |
+
for name, sequences in seq_sets:
|
| 1214 |
+
show(create_alignment_logo(sequences, name))
|
| 1215 |
+
for name, sequences in seq_sets:
|
| 1216 |
+
show(aa_composition_pie(sequences, name))
|
| 1217 |
+
|
| 1218 |
+
if self.modality == "antibody":
|
| 1219 |
+
for name, sequences in seq_sets:
|
| 1220 |
+
show(cdr_logo(sequences, name))
|
| 1221 |
+
|
| 1222 |
+
section_page(
|
| 1223 |
+
"Scatter Plots – Metric relationships",
|
| 1224 |
+
body="""
|
| 1225 |
+
Each scatter page contains two panels:
|
| 1226 |
+
• Left – all designs (grey) with overlays of Top (red) and Diverse (blue).
|
| 1227 |
+
• Right – same but limited to designs passing the RMSD filter.
|
| 1228 |
+
""",
|
| 1229 |
+
)
|
| 1230 |
+
for x, y in extra_pairs:
|
| 1231 |
+
if x in self.df.columns and y in self.df.columns:
|
| 1232 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8.5, 4.5))
|
| 1233 |
+
self._scatter_plus(ax1, self.df, x, y, "All samples")
|
| 1234 |
+
if self.df["pass_filter_rmsd_filter"].sum() > 0:
|
| 1235 |
+
self._scatter_plus(
|
| 1236 |
+
ax2,
|
| 1237 |
+
self.df[self.df["pass_filter_rmsd_filter"]],
|
| 1238 |
+
x,
|
| 1239 |
+
y,
|
| 1240 |
+
"(Designs passing RMSD threshold)",
|
| 1241 |
+
)
|
| 1242 |
+
show(fig)
|
| 1243 |
+
|
| 1244 |
+
section_page(
|
| 1245 |
+
"Metric Distributions – Histograms",
|
| 1246 |
+
body=(
|
| 1247 |
+
"Distribution of each metric across all designs (grey) with overlays for Top-quality (red outline) "
|
| 1248 |
+
"and Diversity-optimised (blue dashed) subsets. The right panel repeats the histogram but only for designs "
|
| 1249 |
+
"that pass the RMSD threshold."
|
| 1250 |
+
),
|
| 1251 |
+
)
|
| 1252 |
+
for m in hist_metrics:
|
| 1253 |
+
if m not in self.df.columns:
|
| 1254 |
+
continue
|
| 1255 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8.5, 4.5))
|
| 1256 |
+
self._hist_plus(
|
| 1257 |
+
ax1, self.df[m], self.df[: self.top_budget][m], self.df_div[m], m, ""
|
| 1258 |
+
)
|
| 1259 |
+
if self.df["pass_filter_rmsd_filter"].sum() > 0:
|
| 1260 |
+
self._hist_plus(
|
| 1261 |
+
ax2,
|
| 1262 |
+
self.df[self.df["pass_filter_rmsd_filter"]][m],
|
| 1263 |
+
self.df[: self.top_budget][m],
|
| 1264 |
+
self.df_div[m],
|
| 1265 |
+
m,
|
| 1266 |
+
" (Designs passing RMSD threshold)",
|
| 1267 |
+
)
|
| 1268 |
+
show(fig)
|
| 1269 |
+
|
| 1270 |
+
if self.num_liability_plots:
|
| 1271 |
+
section_page(
|
| 1272 |
+
"Liability Analysis – Developability flags",
|
| 1273 |
+
body=(
|
| 1274 |
+
"For the first few top designs we compute biochemical liability scores (deamidation, oxidation, etc.) "
|
| 1275 |
+
"and visualise them along the sequence with a heat-map. Dark red indicates positions needing "
|
| 1276 |
+
"attention during optimisation."
|
| 1277 |
+
),
|
| 1278 |
+
)
|
| 1279 |
+
# Plot Liability Heatmaps for Top-budget subset
|
| 1280 |
+
for idx, row in tqdm(
|
| 1281 |
+
enumerate(self.df[: self.num_liability_plots].itertuples(index=False)),
|
| 1282 |
+
desc=f"Making liability plots for top {self.num_liability_plots} sequences.",
|
| 1283 |
+
):
|
| 1284 |
+
seq = row.designed_sequence
|
| 1285 |
+
try:
|
| 1286 |
+
res = compute_liability_scores(
|
| 1287 |
+
[seq], modality=self.modality, peptide_type=self.peptide_type
|
| 1288 |
+
)
|
| 1289 |
+
liab = res.get(seq, {"score": None, "violations": []})
|
| 1290 |
+
fig = plot_seq_liabilities(
|
| 1291 |
+
seq,
|
| 1292 |
+
f"Qualityrank {idx} {row.id}",
|
| 1293 |
+
liab["violations"],
|
| 1294 |
+
total_score=liab["score"],
|
| 1295 |
+
)
|
| 1296 |
+
show(fig)
|
| 1297 |
+
except Exception as e:
|
| 1298 |
+
print(f" Error processing {seq[:20]}: {e}")
|
| 1299 |
+
plt.close("all")
|
| 1300 |
+
continue
|
| 1301 |
+
|
| 1302 |
+
pdf.close()
|
| 1303 |
+
|
| 1304 |
+
print(
|
| 1305 |
+
"A description of metrics and summarizing plots was written to:", pdf_path
|
| 1306 |
+
)
|
| 1307 |
+
|
| 1308 |
+
def _scatter_plus(self, ax, df, x, y, title=""):
|
| 1309 |
+
ax.scatter(
|
| 1310 |
+
df[x],
|
| 1311 |
+
df[y],
|
| 1312 |
+
color="lightgray",
|
| 1313 |
+
alpha=0.4,
|
| 1314 |
+
s=14,
|
| 1315 |
+
zorder=1,
|
| 1316 |
+
)
|
| 1317 |
+
|
| 1318 |
+
ax.scatter(
|
| 1319 |
+
self.df[: self.top_budget][x],
|
| 1320 |
+
self.df[: self.top_budget][y],
|
| 1321 |
+
facecolors="none",
|
| 1322 |
+
edgecolors="red",
|
| 1323 |
+
linewidth=1.5,
|
| 1324 |
+
s=30,
|
| 1325 |
+
zorder=2,
|
| 1326 |
+
alpha=0.5,
|
| 1327 |
+
label="top-quality",
|
| 1328 |
+
)
|
| 1329 |
+
|
| 1330 |
+
if not self.df_div.empty:
|
| 1331 |
+
ax.scatter(
|
| 1332 |
+
self.df_div[x],
|
| 1333 |
+
self.df_div[y],
|
| 1334 |
+
facecolors="none",
|
| 1335 |
+
edgecolors="blue",
|
| 1336 |
+
linewidth=1.5,
|
| 1337 |
+
s=50, # ← slightly larger
|
| 1338 |
+
zorder=3,
|
| 1339 |
+
alpha=0.5,
|
| 1340 |
+
label="quality+diversity",
|
| 1341 |
+
)
|
| 1342 |
+
|
| 1343 |
+
ax.set_xlabel(x)
|
| 1344 |
+
ax.set_ylabel(y)
|
| 1345 |
+
ax.set_title(title)
|
| 1346 |
+
ax.legend()
|
| 1347 |
+
|
| 1348 |
+
def _hist_plus(self, ax, data_all, data_red, data_blue, metric, suffix):
|
| 1349 |
+
# Check if data contains valid values for histogram
|
| 1350 |
+
if data_all is None or len(data_all) == 0 or data_all.isna().all():
|
| 1351 |
+
ax.text(
|
| 1352 |
+
0.5,
|
| 1353 |
+
0.5,
|
| 1354 |
+
f"No valid data for {metric}",
|
| 1355 |
+
ha="center",
|
| 1356 |
+
va="center",
|
| 1357 |
+
transform=ax.transAxes,
|
| 1358 |
+
)
|
| 1359 |
+
ax.set_xlabel(metric)
|
| 1360 |
+
ax.set_ylabel("count")
|
| 1361 |
+
ax.set_title(f"{metric}{suffix} (No data)")
|
| 1362 |
+
return
|
| 1363 |
+
|
| 1364 |
+
# Filter out NaN values for histogram plotting
|
| 1365 |
+
valid_data_all = data_all.dropna()
|
| 1366 |
+
valid_data_red = (
|
| 1367 |
+
data_red.dropna() if data_red is not None else pd.Series(dtype=float)
|
| 1368 |
+
)
|
| 1369 |
+
valid_data_blue = (
|
| 1370 |
+
data_blue.dropna() if data_blue is not None else pd.Series(dtype=float)
|
| 1371 |
+
)
|
| 1372 |
+
|
| 1373 |
+
if len(valid_data_all) == 0:
|
| 1374 |
+
ax.text(
|
| 1375 |
+
0.5,
|
| 1376 |
+
0.5,
|
| 1377 |
+
f"No valid data for {metric}",
|
| 1378 |
+
ha="center",
|
| 1379 |
+
va="center",
|
| 1380 |
+
transform=ax.transAxes,
|
| 1381 |
+
)
|
| 1382 |
+
ax.set_xlabel(metric)
|
| 1383 |
+
ax.set_ylabel("count")
|
| 1384 |
+
ax.set_title(f"{metric}{suffix} (No data)")
|
| 1385 |
+
return
|
| 1386 |
+
|
| 1387 |
+
# Create histogram with valid data
|
| 1388 |
+
ax.hist(valid_data_all, bins=30, color="lightgray", alpha=0.6, label="all")
|
| 1389 |
+
|
| 1390 |
+
if len(valid_data_red) > 0:
|
| 1391 |
+
ax.hist(
|
| 1392 |
+
valid_data_red,
|
| 1393 |
+
bins=30,
|
| 1394 |
+
histtype="step",
|
| 1395 |
+
linewidth=1.7,
|
| 1396 |
+
color="red",
|
| 1397 |
+
label="top-quality",
|
| 1398 |
+
)
|
| 1399 |
+
if len(data_blue) and len(valid_data_blue) > 0:
|
| 1400 |
+
ax.hist(
|
| 1401 |
+
valid_data_blue,
|
| 1402 |
+
bins=30,
|
| 1403 |
+
histtype="step",
|
| 1404 |
+
linewidth=1.7,
|
| 1405 |
+
color="blue",
|
| 1406 |
+
linestyle="--",
|
| 1407 |
+
label="quality+diversity",
|
| 1408 |
+
)
|
| 1409 |
+
|
| 1410 |
+
ax.set_xlabel(metric)
|
| 1411 |
+
ax.set_ylabel("count")
|
| 1412 |
+
ax.set_title(f"{metric}{suffix}")
|
| 1413 |
+
ax.legend()
|
model/boltzgen/task/filter/seqplot_utils.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
from matplotlib import pyplot as plt
|
| 4 |
+
import matplotlib.colors as mcolors
|
| 5 |
+
from matplotlib.patches import Patch
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import logomaker
|
| 9 |
+
|
| 10 |
+
from boltzgen.data import const
|
| 11 |
+
|
| 12 |
+
# Define amino acid properties for peptide visualization
|
| 13 |
+
HYDROPHOBIC = set("ACILMFWV")
|
| 14 |
+
NEUTRAL = set("GHPTY")
|
| 15 |
+
HYDROPHILIC = set("RNDQEK")
|
| 16 |
+
POSITIVE = set("RHK")
|
| 17 |
+
NEGATIVE = set("DE")
|
| 18 |
+
AA20 = list("ACDEFGHIKLMNPQRSTVWY")
|
| 19 |
+
|
| 20 |
+
COLOR_TABLE = {
|
| 21 |
+
("hydrophobic", "positive"): "#d62728",
|
| 22 |
+
("hydrophobic", "negative"): "#1f77b4",
|
| 23 |
+
("hydrophobic", "uncharged"): "#2ca02c",
|
| 24 |
+
("neutral", "positive"): "#ff7f0e",
|
| 25 |
+
("neutral", "negative"): "#17becf",
|
| 26 |
+
("neutral", "uncharged"): "#bcbd22",
|
| 27 |
+
("hydrophilic", "positive"): "#e377c2",
|
| 28 |
+
("hydrophilic", "negative"): "#1f99d4",
|
| 29 |
+
("hydrophilic", "uncharged"): "#9467bd",
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _hydropathy_class(res):
|
| 34 |
+
if res in HYDROPHOBIC:
|
| 35 |
+
return "hydrophobic"
|
| 36 |
+
elif res in HYDROPHILIC:
|
| 37 |
+
return "hydrophilic"
|
| 38 |
+
else:
|
| 39 |
+
return "neutral"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _charge_class(res):
|
| 43 |
+
if res in POSITIVE:
|
| 44 |
+
return "positive"
|
| 45 |
+
elif res in NEGATIVE:
|
| 46 |
+
return "negative"
|
| 47 |
+
else:
|
| 48 |
+
return "uncharged"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
hydrophobicity_colors = {
|
| 52 |
+
aa: COLOR_TABLE[(_hydropathy_class(aa), _charge_class(aa))] for aa in AA20
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def create_alignment_logo(sequences, name, width=10):
|
| 57 |
+
"""Create sequence logo from aligned sequences."""
|
| 58 |
+
|
| 59 |
+
aligned_sequences, _ = align_peptide_sequences(sequences)
|
| 60 |
+
if not aligned_sequences or len(aligned_sequences) < 2:
|
| 61 |
+
msg = f"Warning: Not enough sequences for logo: {len(aligned_sequences)}"
|
| 62 |
+
print(msg)
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
# Count amino acids at each position
|
| 66 |
+
max_len = max(len(seq) for seq in aligned_sequences)
|
| 67 |
+
counts = {aa: np.zeros(max_len, dtype=int) for aa in AA20}
|
| 68 |
+
|
| 69 |
+
for i, seq in enumerate(aligned_sequences):
|
| 70 |
+
for pos, aa in enumerate(seq):
|
| 71 |
+
if pos < max_len and aa in AA20:
|
| 72 |
+
counts[aa][pos] += 1
|
| 73 |
+
|
| 74 |
+
# Convert to DataFrame
|
| 75 |
+
counts_df = pd.DataFrame(counts)
|
| 76 |
+
counts_df.index.name = "position"
|
| 77 |
+
|
| 78 |
+
return draw_logo(counts_df, name, width=width)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def aa_composition_pie(sequences, name):
|
| 82 |
+
# Generate amino acid composition analysis
|
| 83 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
|
| 84 |
+
|
| 85 |
+
# Hydrophobicity distribution
|
| 86 |
+
hydrophobicity_counts = {
|
| 87 |
+
"hydrophobic": 0,
|
| 88 |
+
"neutral": 0,
|
| 89 |
+
"hydrophilic": 0,
|
| 90 |
+
}
|
| 91 |
+
for seq in sequences:
|
| 92 |
+
for aa in seq:
|
| 93 |
+
if aa in AA20:
|
| 94 |
+
hydrophobicity_counts[_hydropathy_class(aa)] += 1
|
| 95 |
+
|
| 96 |
+
ax1.pie(
|
| 97 |
+
hydrophobicity_counts.values(),
|
| 98 |
+
labels=hydrophobicity_counts.keys(),
|
| 99 |
+
autopct="%1.1f%%",
|
| 100 |
+
)
|
| 101 |
+
ax1.set_title(f"{name}\n Hydrophobicity Distribution", y=1.02, wrap=True)
|
| 102 |
+
|
| 103 |
+
# Charge distribution
|
| 104 |
+
charge_counts = {"positive": 0, "negative": 0, "uncharged": 0}
|
| 105 |
+
for seq in sequences:
|
| 106 |
+
for aa in seq:
|
| 107 |
+
if aa in AA20:
|
| 108 |
+
charge_counts[_charge_class(aa)] += 1
|
| 109 |
+
|
| 110 |
+
ax2.pie(
|
| 111 |
+
charge_counts.values(),
|
| 112 |
+
labels=charge_counts.keys(),
|
| 113 |
+
autopct="%1.1f%%",
|
| 114 |
+
)
|
| 115 |
+
ax2.set_title(f"{name}\n Charge Distribution", y=1.02, wrap=True)
|
| 116 |
+
fig.tight_layout(rect=[0, 0, 1, 0.92])
|
| 117 |
+
return fig
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def align_peptide_sequences(sequences):
|
| 121 |
+
"""Align peptide sequences using BioPython's multiple sequence alignment."""
|
| 122 |
+
|
| 123 |
+
if len(sequences) < 2:
|
| 124 |
+
print("Warning: Only one sequence, no alignment needed")
|
| 125 |
+
return sequences, []
|
| 126 |
+
|
| 127 |
+
# Fallback to pairwise alignment (less optimal but functional)
|
| 128 |
+
from Bio import Align
|
| 129 |
+
from Bio.Align import substitution_matrices
|
| 130 |
+
|
| 131 |
+
# Use BLOSUM62 matrix for protein alignment
|
| 132 |
+
matrix = substitution_matrices.load("BLOSUM62")
|
| 133 |
+
|
| 134 |
+
# Create pairwise aligner
|
| 135 |
+
aligner = Align.PairwiseAligner()
|
| 136 |
+
aligner.substitution_matrix = matrix
|
| 137 |
+
aligner.open_gap_score = -11
|
| 138 |
+
aligner.extend_gap_score = -1
|
| 139 |
+
|
| 140 |
+
# Start with first sequence as reference
|
| 141 |
+
aligned_seqs = [sequences[0]]
|
| 142 |
+
alignment_scores = []
|
| 143 |
+
|
| 144 |
+
# Align each sequence to the reference
|
| 145 |
+
for i, seq in enumerate(sequences[1:], 1):
|
| 146 |
+
try:
|
| 147 |
+
alignment = aligner.align(sequences[0], seq)
|
| 148 |
+
if alignment:
|
| 149 |
+
# Get the QUERY sequence (the different one) with gaps
|
| 150 |
+
aligned_seq = str(alignment[0].query)
|
| 151 |
+
|
| 152 |
+
aligned_seqs.append(aligned_seq)
|
| 153 |
+
alignment_scores.append(alignment[0].score)
|
| 154 |
+
else:
|
| 155 |
+
aligned_seqs.append(seq)
|
| 156 |
+
alignment_scores.append(0)
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f" DEBUG: Alignment failed: {e}")
|
| 159 |
+
# Fallback: just add the sequence as-is
|
| 160 |
+
aligned_seqs.append(seq)
|
| 161 |
+
alignment_scores.append(0)
|
| 162 |
+
return aligned_seqs, alignment_scores
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def create_temp_fasta(sequences, names):
|
| 166 |
+
"""Create a temporary FASTA file from sequences."""
|
| 167 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".fasta", delete=False) as f:
|
| 168 |
+
for i, (seq, name) in enumerate(zip(sequences, names)):
|
| 169 |
+
f.write(f">{name}\n{seq}\n")
|
| 170 |
+
return f.name
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def build_region_alignment(chains, region_name):
|
| 174 |
+
"""Build alignment for a specific CDR region."""
|
| 175 |
+
if not chains:
|
| 176 |
+
return [], []
|
| 177 |
+
|
| 178 |
+
all_positions = set()
|
| 179 |
+
per_chain_pos_to_aa = []
|
| 180 |
+
for ch in chains:
|
| 181 |
+
reg = ch.regions.get(region_name, {})
|
| 182 |
+
pos_to_aa = {str(pos): aa for pos, aa in reg.items()}
|
| 183 |
+
per_chain_pos_to_aa.append(pos_to_aa)
|
| 184 |
+
all_positions.update(pos_to_aa.keys())
|
| 185 |
+
|
| 186 |
+
def pos_key(p):
|
| 187 |
+
p = p.replace("H", "")
|
| 188 |
+
num = ""
|
| 189 |
+
ins = ""
|
| 190 |
+
for c in p:
|
| 191 |
+
if c.isdigit():
|
| 192 |
+
num += c
|
| 193 |
+
else:
|
| 194 |
+
ins += c
|
| 195 |
+
return (int(num), ins or "")
|
| 196 |
+
|
| 197 |
+
ordered_positions = sorted(all_positions, key=pos_key)
|
| 198 |
+
msa = []
|
| 199 |
+
ids = []
|
| 200 |
+
for i, ch in enumerate(chains):
|
| 201 |
+
pos_to_aa = per_chain_pos_to_aa[i]
|
| 202 |
+
row = "".join(pos_to_aa.get(p, "-") for p in ordered_positions)
|
| 203 |
+
name = ch.name if getattr(ch, "name", None) else f"seq_{i + 1}"
|
| 204 |
+
ids.append(name)
|
| 205 |
+
msa.append(row)
|
| 206 |
+
return ids, msa
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def counts_matrix_from_msa(msa, alphabet):
|
| 210 |
+
"""Compute counts matrix from MSA."""
|
| 211 |
+
if not msa:
|
| 212 |
+
return pd.DataFrame()
|
| 213 |
+
|
| 214 |
+
msa = [row.upper() for row in msa]
|
| 215 |
+
L = len(msa[0])
|
| 216 |
+
counts = {aa: np.zeros(L, dtype=int) for aa in alphabet}
|
| 217 |
+
|
| 218 |
+
for row in msa:
|
| 219 |
+
assert len(row) == L, "All rows must have same length"
|
| 220 |
+
for j, aa in enumerate(row):
|
| 221 |
+
if aa in ["-", "X", "J", "U", "O"]:
|
| 222 |
+
continue
|
| 223 |
+
if aa in counts:
|
| 224 |
+
counts[aa][j] += 1
|
| 225 |
+
|
| 226 |
+
df = pd.DataFrame(counts)
|
| 227 |
+
df.index.name = "position"
|
| 228 |
+
return df
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def draw_logo(counts, title, width=10):
|
| 232 |
+
"""Draw sequence logo using logomaker."""
|
| 233 |
+
probs = counts.div(counts.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)
|
| 234 |
+
entropy = (
|
| 235 |
+
-(probs.replace(0, np.nan) * np.log2(probs.replace(0, np.nan)))
|
| 236 |
+
.sum(axis=1)
|
| 237 |
+
.fillna(0)
|
| 238 |
+
)
|
| 239 |
+
info = np.log2(20) - entropy
|
| 240 |
+
heights = probs.mul(info, axis=0)
|
| 241 |
+
|
| 242 |
+
fig, ax = plt.subplots(figsize=(width, 4))
|
| 243 |
+
logo = logomaker.Logo(
|
| 244 |
+
heights,
|
| 245 |
+
color_scheme=hydrophobicity_colors,
|
| 246 |
+
shade_below=0.5,
|
| 247 |
+
fade_below=0.5,
|
| 248 |
+
vpad=0.05,
|
| 249 |
+
width=0.8,
|
| 250 |
+
ax=ax,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
legend_elements = []
|
| 254 |
+
for (hydro, charge), color in COLOR_TABLE.items():
|
| 255 |
+
label = f"{hydro.capitalize()} + {charge}"
|
| 256 |
+
legend_elements.append(Patch(facecolor=color, edgecolor="black", label=label))
|
| 257 |
+
|
| 258 |
+
ax.legend(handles=legend_elements, bbox_to_anchor=(1.05, 1), loc="upper left")
|
| 259 |
+
ax.set_title(title)
|
| 260 |
+
ax.set_xlabel("Aligned position")
|
| 261 |
+
ax.set_ylabel("Information (bits)")
|
| 262 |
+
|
| 263 |
+
plt.tight_layout()
|
| 264 |
+
return fig
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def cdr_logo(sequences, name):
|
| 268 |
+
from abnumber import Chain
|
| 269 |
+
|
| 270 |
+
# Create temporary FASTA
|
| 271 |
+
names = [f"seq_{i + 1}" for i in range(len(sequences))]
|
| 272 |
+
temp_fasta = create_temp_fasta(sequences, names)
|
| 273 |
+
|
| 274 |
+
# Load chains using AbNumber
|
| 275 |
+
chains = []
|
| 276 |
+
for ch in Chain.from_fasta(
|
| 277 |
+
temp_fasta,
|
| 278 |
+
scheme="chothia",
|
| 279 |
+
cdr_definition="chothia",
|
| 280 |
+
as_generator=True,
|
| 281 |
+
allowed_species=["alpaca"],
|
| 282 |
+
):
|
| 283 |
+
if ch.chain_type == "H":
|
| 284 |
+
chains.append(ch)
|
| 285 |
+
|
| 286 |
+
# Generate composite figure
|
| 287 |
+
fig, axes = plt.subplots(3, 1, figsize=(12, 8), constrained_layout=True)
|
| 288 |
+
for ax, cdr in zip(axes, ["CDR1", "CDR2", "CDR3"]):
|
| 289 |
+
ids, msa = build_region_alignment(chains, cdr)
|
| 290 |
+
if not msa:
|
| 291 |
+
ax.set_axis_off()
|
| 292 |
+
continue
|
| 293 |
+
|
| 294 |
+
counts = counts_matrix_from_msa(msa, AA20)
|
| 295 |
+
probs = counts.div(counts.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)
|
| 296 |
+
entropy = (
|
| 297 |
+
-(probs.replace(0, np.nan) * np.log2(probs.replace(0, np.nan)))
|
| 298 |
+
.sum(axis=1)
|
| 299 |
+
.fillna(0)
|
| 300 |
+
)
|
| 301 |
+
info = np.log2(20) - entropy
|
| 302 |
+
heights = probs.mul(info, axis=0)
|
| 303 |
+
|
| 304 |
+
logo = logomaker.Logo(
|
| 305 |
+
heights,
|
| 306 |
+
ax=ax,
|
| 307 |
+
color_scheme=hydrophobicity_colors,
|
| 308 |
+
shade_below=0.5,
|
| 309 |
+
fade_below=0.5,
|
| 310 |
+
vpad=0.05,
|
| 311 |
+
width=0.8,
|
| 312 |
+
)
|
| 313 |
+
ax.set_title(f"{name} {cdr}")
|
| 314 |
+
ax.set_xlabel("")
|
| 315 |
+
ax.set_ylabel("bits")
|
| 316 |
+
|
| 317 |
+
# Add legend
|
| 318 |
+
legend_elements = [
|
| 319 |
+
Patch(
|
| 320 |
+
facecolor=color,
|
| 321 |
+
edgecolor="black",
|
| 322 |
+
label=f"{hydro.capitalize()} + {charge}",
|
| 323 |
+
)
|
| 324 |
+
for (hydro, charge), color in COLOR_TABLE.items()
|
| 325 |
+
]
|
| 326 |
+
fig.legend(
|
| 327 |
+
handles=legend_elements,
|
| 328 |
+
loc="center left",
|
| 329 |
+
bbox_to_anchor=(1.02, 0.5),
|
| 330 |
+
borderaxespad=0.0,
|
| 331 |
+
)
|
| 332 |
+
os.unlink(temp_fasta)
|
| 333 |
+
return fig
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def plot_seq_liabilities(
|
| 337 |
+
sequence,
|
| 338 |
+
name,
|
| 339 |
+
violations,
|
| 340 |
+
total_score=None,
|
| 341 |
+
):
|
| 342 |
+
"""
|
| 343 |
+
Plot sequence with colored backgrounds for each motif region.
|
| 344 |
+
|
| 345 |
+
Args:
|
| 346 |
+
sequence: The amino acid sequence
|
| 347 |
+
violations: List of violation dictionaries
|
| 348 |
+
total_score: Total liability score
|
| 349 |
+
"""
|
| 350 |
+
n = len(sequence)
|
| 351 |
+
sev_arr = [0] * n
|
| 352 |
+
violation_types = {} # Track unique violation types and their max severity
|
| 353 |
+
|
| 354 |
+
# Process violations and compute positions for those that don't have explicit pos/len
|
| 355 |
+
for v in violations:
|
| 356 |
+
motif_name = v["motif"]
|
| 357 |
+
|
| 358 |
+
# Handle violations that need position computation
|
| 359 |
+
if motif_name == "ConsecIdentical" and v.get("pos") is None:
|
| 360 |
+
# Find consecutive identical residues
|
| 361 |
+
for i in range(n - 1):
|
| 362 |
+
if sequence[i] == sequence[i + 1]:
|
| 363 |
+
# Color both positions
|
| 364 |
+
sev_arr[i] = max(sev_arr[i], v["severity"])
|
| 365 |
+
sev_arr[i + 1] = max(sev_arr[i + 1], v["severity"])
|
| 366 |
+
elif motif_name == "LongHydrophobic" and v.get("pos") is None:
|
| 367 |
+
# Find stretches of >4 consecutive hydrophobic residues
|
| 368 |
+
hydrophobic_residues = set("FILVWY")
|
| 369 |
+
for i in range(n - 4):
|
| 370 |
+
stretch = sequence[i : i + 5]
|
| 371 |
+
if all(aa in hydrophobic_residues for aa in stretch):
|
| 372 |
+
# Color the entire stretch
|
| 373 |
+
for j in range(i, i + 5):
|
| 374 |
+
if j < n:
|
| 375 |
+
sev_arr[j] = max(sev_arr[j], v["severity"])
|
| 376 |
+
elif v.get("pos") and v.get("len"):
|
| 377 |
+
# Standard violations with explicit positions
|
| 378 |
+
for i in range(v["pos"] - 1, v["pos"] - 1 + v["len"]):
|
| 379 |
+
if 0 <= i < n: # Ensure index is within bounds
|
| 380 |
+
sev_arr[i] = max(sev_arr[i], v["severity"])
|
| 381 |
+
|
| 382 |
+
# Track violation types for legend
|
| 383 |
+
if motif_name not in violation_types:
|
| 384 |
+
violation_types[motif_name] = v["severity"]
|
| 385 |
+
else:
|
| 386 |
+
violation_types[motif_name] = max(
|
| 387 |
+
violation_types[motif_name], v["severity"]
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
cmap = mcolors.LinearSegmentedColormap.from_list(
|
| 391 |
+
"HeatMap", ["white", "yellow", "red"]
|
| 392 |
+
)
|
| 393 |
+
norm = mcolors.Normalize(vmin=0, vmax=max(const.liability_severity.values()))
|
| 394 |
+
|
| 395 |
+
# Better figure size calculation to prevent letter cropping
|
| 396 |
+
# Ensure minimum width for short sequences and proper height
|
| 397 |
+
min_width = max(8, n * 0.3) # At least 8 inches wide, or 0.3 inches per residue
|
| 398 |
+
fig_height = 4 # Fixed height for consistency
|
| 399 |
+
|
| 400 |
+
# Create figure with space for legend
|
| 401 |
+
fig, (ax, ax_legend) = plt.subplots(
|
| 402 |
+
2, 1, figsize=(min_width, fig_height), gridspec_kw={"height_ratios": [2.5, 1]}
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
# Main sequence plot
|
| 406 |
+
for idx, aa in enumerate(sequence):
|
| 407 |
+
color = cmap(norm(sev_arr[idx]))
|
| 408 |
+
ax.text(
|
| 409 |
+
idx,
|
| 410 |
+
0.5,
|
| 411 |
+
aa,
|
| 412 |
+
ha="center",
|
| 413 |
+
va="center",
|
| 414 |
+
fontsize=12,
|
| 415 |
+
bbox=dict(facecolor=color, edgecolor="none", boxstyle="square,pad=0.1"),
|
| 416 |
+
)
|
| 417 |
+
ax.set_xlim(-0.5, n - 0.5)
|
| 418 |
+
ax.set_ylim(0, 1)
|
| 419 |
+
ax.axis("off")
|
| 420 |
+
|
| 421 |
+
if total_score is not None:
|
| 422 |
+
name += f" (Score: {total_score})"
|
| 423 |
+
ax.set_title(name)
|
| 424 |
+
|
| 425 |
+
# Colorbar
|
| 426 |
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
| 427 |
+
sm.set_array([])
|
| 428 |
+
plt.colorbar(sm, ax=ax, orientation="horizontal", pad=0.2, label="Severity")
|
| 429 |
+
|
| 430 |
+
# Legend showing violation types with motif patterns
|
| 431 |
+
if violation_types:
|
| 432 |
+
ax_legend.axis("off")
|
| 433 |
+
legend_items = []
|
| 434 |
+
legend_labels = []
|
| 435 |
+
|
| 436 |
+
# Sort by severity (descending)
|
| 437 |
+
sorted_violations = sorted(
|
| 438 |
+
violation_types.items(), key=lambda x: x[1], reverse=True
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
# Motif pattern mapping for legend
|
| 442 |
+
motif_patterns = {
|
| 443 |
+
"DeAmdH": "N[GS]",
|
| 444 |
+
"DeAmdM": "N[AHNT]",
|
| 445 |
+
"DeAmdL": "[STK]N",
|
| 446 |
+
"Ngly": "N[^P][ST]",
|
| 447 |
+
"Isom": "D[DG HST]",
|
| 448 |
+
"Isomer": "DG|DS|DD",
|
| 449 |
+
"FragH": "DP",
|
| 450 |
+
"FragM": "TS",
|
| 451 |
+
"TrpOx": "W",
|
| 452 |
+
"MetOx": "M",
|
| 453 |
+
"Hydro": "NP",
|
| 454 |
+
"IntBind": "GPR|RGD|RYD|LDV|DGE|KGD|NGR",
|
| 455 |
+
"Polyreactive": "GGG|WWW|GG|RR|VG|VVV|YY|WxW",
|
| 456 |
+
"AggPatch": "FHW",
|
| 457 |
+
"ViscPatch": "HYF|HWH",
|
| 458 |
+
"UnpairedCys": "C",
|
| 459 |
+
"HighNetCharge": "net charge > +1",
|
| 460 |
+
"AspBridge": "N[GSQA]",
|
| 461 |
+
"AspCleave": "D[PGS]",
|
| 462 |
+
"NTCycl": "^[QN]",
|
| 463 |
+
"ProtTryp": "[KR](?=.)",
|
| 464 |
+
"DPP4": "^[PX]?[AP]",
|
| 465 |
+
"CysOx": "C",
|
| 466 |
+
"HydroPatch": "[FILVWY]{3,}",
|
| 467 |
+
"LowHydrophilic": "< 40% hydrophilic",
|
| 468 |
+
"ConsecIdentical": "consecutive identical",
|
| 469 |
+
"LongHydrophobic": "> 4 consecutive hydrophobic",
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
for motif_name, severity in sorted_violations:
|
| 473 |
+
color = cmap(norm(severity))
|
| 474 |
+
legend_items.append(
|
| 475 |
+
plt.Rectangle((0, 0), 1, 1, facecolor=color, edgecolor="black")
|
| 476 |
+
)
|
| 477 |
+
pattern = motif_patterns.get(motif_name, "unknown")
|
| 478 |
+
legend_labels.append(f"{motif_name}: {pattern}")
|
| 479 |
+
|
| 480 |
+
ax_legend.legend(
|
| 481 |
+
legend_items,
|
| 482 |
+
legend_labels,
|
| 483 |
+
loc="center",
|
| 484 |
+
title="Violations",
|
| 485 |
+
ncol=1,
|
| 486 |
+
fontsize=8,
|
| 487 |
+
)
|
| 488 |
+
else:
|
| 489 |
+
# No violations detected
|
| 490 |
+
ax_legend.axis("off")
|
| 491 |
+
ax_legend.text(
|
| 492 |
+
0.5,
|
| 493 |
+
0.5,
|
| 494 |
+
"No violations detected",
|
| 495 |
+
ha="center",
|
| 496 |
+
va="center",
|
| 497 |
+
fontsize=12,
|
| 498 |
+
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen", alpha=0.7),
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
ax_legend.set_title("Violations", fontsize=10)
|
| 502 |
+
|
| 503 |
+
plt.tight_layout()
|
| 504 |
+
|
| 505 |
+
return fig
|
model/boltzgen/task/predict/__init__.py
ADDED
|
File without changes
|
model/boltzgen/task/predict/data_from_generated.py
ADDED
|
@@ -0,0 +1,882 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import random
|
| 4 |
+
import re
|
| 5 |
+
import warnings
|
| 6 |
+
from typing import Dict, List, Optional
|
| 7 |
+
from collections import defaultdict
|
| 8 |
+
from rdkit.Chem import Mol
|
| 9 |
+
import pickle
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import pytorch_lightning as pl
|
| 13 |
+
import torch
|
| 14 |
+
from torch import Tensor
|
| 15 |
+
from torch.utils.data import DataLoader
|
| 16 |
+
|
| 17 |
+
from boltzgen.data import const
|
| 18 |
+
from boltzgen.data.data import Input, Structure, Tokenized
|
| 19 |
+
from boltzgen.data.feature.featurizer import Featurizer
|
| 20 |
+
from boltzgen.data.mol import load_canonicals, load_molecules
|
| 21 |
+
from boltzgen.data.pad import pad_to_max
|
| 22 |
+
from boltzgen.data.parse import mmcif
|
| 23 |
+
from boltzgen.data.parse.pdb_parser import parse_pdb
|
| 24 |
+
from boltzgen.data.template.features import (
|
| 25 |
+
load_dummy_templates,
|
| 26 |
+
)
|
| 27 |
+
from boltzgen.data.parse.schema import parse_redesign_yaml
|
| 28 |
+
from boltzgen.data.tokenize.tokenizer import Tokenizer
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class DataFetchException(Exception):
|
| 32 |
+
pass
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class DataConfig:
|
| 37 |
+
"""Data configuration."""
|
| 38 |
+
|
| 39 |
+
num_targets: int
|
| 40 |
+
samples_per_target: int
|
| 41 |
+
moldir: str
|
| 42 |
+
tokenizer: Tokenizer
|
| 43 |
+
featurizer: Featurizer
|
| 44 |
+
batch_size: int
|
| 45 |
+
num_workers: int
|
| 46 |
+
pin_memory: bool
|
| 47 |
+
suffix: str = ".cif"
|
| 48 |
+
suffix_native: str = "_native.cif"
|
| 49 |
+
suffix_metadata: str = ".npz"
|
| 50 |
+
target_id_regex: str = (
|
| 51 |
+
r"^(?:(?:sample\d+_|batch\d+_|rank\d+_)+)?([^_]+)(?:_[^_]+)*?(?:_(?:gen))*$"
|
| 52 |
+
)
|
| 53 |
+
design: bool = False
|
| 54 |
+
# Featurizer args (if design is True these should match with training config):
|
| 55 |
+
backbone_only: bool = False
|
| 56 |
+
atom14: bool = True
|
| 57 |
+
max_seqs: int = 1
|
| 58 |
+
inverse_fold: bool = False
|
| 59 |
+
extra_mol_dir: Optional[str] = None
|
| 60 |
+
disulfide_prob: float = 1.0
|
| 61 |
+
disulfide_on: bool = False
|
| 62 |
+
design_mask_override: Optional[str] = None
|
| 63 |
+
multiplicity: int = 1
|
| 64 |
+
return_designfolding: bool = False
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def collate(data: List[Dict[str, Tensor]]) -> Dict[str, Tensor]:
|
| 68 |
+
"""Collate the data.
|
| 69 |
+
|
| 70 |
+
Parameters
|
| 71 |
+
----------
|
| 72 |
+
data : List[Dict[str, Tensor]]
|
| 73 |
+
The data to collate.
|
| 74 |
+
|
| 75 |
+
Returns
|
| 76 |
+
-------
|
| 77 |
+
Dict[str, Tensor]
|
| 78 |
+
The collated data.
|
| 79 |
+
|
| 80 |
+
"""
|
| 81 |
+
# Get the keys
|
| 82 |
+
keys = data[0].keys()
|
| 83 |
+
|
| 84 |
+
# Collate the data
|
| 85 |
+
collated = {}
|
| 86 |
+
for key in keys:
|
| 87 |
+
values = [d[key] for d in data]
|
| 88 |
+
|
| 89 |
+
if key not in [
|
| 90 |
+
"metadata",
|
| 91 |
+
"str_gen",
|
| 92 |
+
"id",
|
| 93 |
+
"path",
|
| 94 |
+
"native_metadata",
|
| 95 |
+
"native_str_gen",
|
| 96 |
+
"native_id",
|
| 97 |
+
"native_path",
|
| 98 |
+
"exception",
|
| 99 |
+
"native_exception",
|
| 100 |
+
"skip",
|
| 101 |
+
"native_skip",
|
| 102 |
+
"structure_bonds",
|
| 103 |
+
"native_structure_bonds",
|
| 104 |
+
"extra_mols",
|
| 105 |
+
"native_extra_mols",
|
| 106 |
+
"structure",
|
| 107 |
+
"tokenized",
|
| 108 |
+
"data_sample_idx",
|
| 109 |
+
]:
|
| 110 |
+
# Check if all have the same shape
|
| 111 |
+
shape = values[0].shape
|
| 112 |
+
if not all(v.shape == shape for v in values):
|
| 113 |
+
values = pad_to_max(values, 0)
|
| 114 |
+
else:
|
| 115 |
+
values = torch.stack(values, dim=0)
|
| 116 |
+
|
| 117 |
+
# Stack the values
|
| 118 |
+
collated[key] = values
|
| 119 |
+
|
| 120 |
+
return collated
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@dataclass(frozen=True)
|
| 124 |
+
class TemplateInfo:
|
| 125 |
+
"""TemplateInfo datatype."""
|
| 126 |
+
|
| 127 |
+
name: str
|
| 128 |
+
query_chain: str
|
| 129 |
+
query_st: int
|
| 130 |
+
query_en: int
|
| 131 |
+
template_chain: str
|
| 132 |
+
template_st: int
|
| 133 |
+
template_en: int
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def template_from_tokens(
|
| 137 |
+
tokenized: Tokenized,
|
| 138 |
+
token_mask: np.ndarray[bool],
|
| 139 |
+
tdim: int = 1,
|
| 140 |
+
) -> dict[str, torch.Tensor]:
|
| 141 |
+
"""Get template features where the tokens specified in token_mask have their structure specified."""
|
| 142 |
+
# Get num token
|
| 143 |
+
num_tokens = len(tokenized.tokens)
|
| 144 |
+
|
| 145 |
+
# Allocate features
|
| 146 |
+
res_type = np.zeros((tdim, num_tokens), dtype=np.int64)
|
| 147 |
+
frame_rot = np.zeros((tdim, num_tokens, 3, 3), dtype=np.float32)
|
| 148 |
+
frame_t = np.zeros((tdim, num_tokens, 3), dtype=np.float32)
|
| 149 |
+
cb_coords = np.zeros((tdim, num_tokens, 3), dtype=np.float32)
|
| 150 |
+
ca_coords = np.zeros((tdim, num_tokens, 3), dtype=np.float32)
|
| 151 |
+
frame_mask = np.zeros((tdim, num_tokens), dtype=np.float32)
|
| 152 |
+
cb_mask = np.zeros((tdim, num_tokens), dtype=np.float32)
|
| 153 |
+
template_mask = np.zeros((tdim, num_tokens), dtype=np.float32)
|
| 154 |
+
query_to_template = np.zeros((tdim, num_tokens), dtype=np.int64)
|
| 155 |
+
visibility_ids = np.zeros((tdim, num_tokens), dtype=np.float32)
|
| 156 |
+
|
| 157 |
+
# Now create features per token
|
| 158 |
+
template_indices = np.where(token_mask)[0]
|
| 159 |
+
for token_idx in template_indices:
|
| 160 |
+
token = tokenized.tokens[token_idx]
|
| 161 |
+
res_type[:, token_idx] = token["res_type"]
|
| 162 |
+
frame_rot[:, token_idx] = token["frame_rot"].reshape(3, 3)
|
| 163 |
+
frame_t[:, token_idx] = token["frame_t"]
|
| 164 |
+
cb_coords[:, token_idx] = token["disto_coords"]
|
| 165 |
+
ca_coords[:, token_idx] = token["center_coords"]
|
| 166 |
+
cb_mask[:, token_idx] = token["disto_mask"]
|
| 167 |
+
frame_mask[:, token_idx] = token["frame_mask"]
|
| 168 |
+
template_mask[:, token_idx] = 1.0
|
| 169 |
+
visibility_ids[:, token_idx] = 1
|
| 170 |
+
|
| 171 |
+
# Convert to one-hot
|
| 172 |
+
res_type = torch.from_numpy(res_type)
|
| 173 |
+
res_type = torch.nn.functional.one_hot(res_type, num_classes=const.num_tokens)
|
| 174 |
+
|
| 175 |
+
return {
|
| 176 |
+
"template_restype": res_type,
|
| 177 |
+
"template_frame_rot": torch.from_numpy(frame_rot),
|
| 178 |
+
"template_frame_t": torch.from_numpy(frame_t),
|
| 179 |
+
"template_cb": torch.from_numpy(cb_coords),
|
| 180 |
+
"template_ca": torch.from_numpy(ca_coords),
|
| 181 |
+
"template_mask_cb": torch.from_numpy(cb_mask),
|
| 182 |
+
"template_mask_frame": torch.from_numpy(frame_mask),
|
| 183 |
+
"template_mask": torch.from_numpy(template_mask),
|
| 184 |
+
"query_to_template": torch.from_numpy(query_to_template),
|
| 185 |
+
"visibility_ids": torch.from_numpy(visibility_ids),
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class FromGeneratedDataset(torch.utils.data.Dataset):
|
| 190 |
+
def __init__(
|
| 191 |
+
self,
|
| 192 |
+
generated_paths: List[Path],
|
| 193 |
+
metadata_paths: List[Path],
|
| 194 |
+
native_paths: List[Path],
|
| 195 |
+
moldir: Path,
|
| 196 |
+
canonicals: dict[str, Mol],
|
| 197 |
+
tokenizer: Tokenizer,
|
| 198 |
+
featurizer: Featurizer,
|
| 199 |
+
return_native: bool = False,
|
| 200 |
+
reference_metadata_dir: Optional[Path] = None,
|
| 201 |
+
target_templates: bool = False,
|
| 202 |
+
design_mask_templates: bool = False,
|
| 203 |
+
compute_affinity: bool = False,
|
| 204 |
+
design: bool = False,
|
| 205 |
+
backbone_only: bool = False,
|
| 206 |
+
atom14: bool = True,
|
| 207 |
+
max_seqs: int = 1,
|
| 208 |
+
inverse_fold: bool = False,
|
| 209 |
+
extra_mol_dir: Optional[Path] = None,
|
| 210 |
+
extra_features: Optional[List[str]] = None,
|
| 211 |
+
disulfide_prob: float = 1.0,
|
| 212 |
+
disulfide_on: bool = False,
|
| 213 |
+
design_mask_override: Optional[str] = None,
|
| 214 |
+
use_new_design_mask: bool = False,
|
| 215 |
+
multiplicity: int = 1,
|
| 216 |
+
return_designfolding=False,
|
| 217 |
+
) -> None:
|
| 218 |
+
"""
|
| 219 |
+
Parameters
|
| 220 |
+
----------
|
| 221 |
+
design : bool
|
| 222 |
+
Set to True if this dataset is used to make predictions over (i.e. design some parts
|
| 223 |
+
of the structure). Set to False if this dataset is used to only evaluate the predictions
|
| 224 |
+
under the paths (i.e. no design is done).
|
| 225 |
+
"""
|
| 226 |
+
super().__init__()
|
| 227 |
+
self.tokenizer = tokenizer
|
| 228 |
+
self.moldir = moldir
|
| 229 |
+
self.canonicals = canonicals
|
| 230 |
+
self.featurizer = featurizer
|
| 231 |
+
self.metadata_paths = metadata_paths
|
| 232 |
+
self.generated_paths = generated_paths
|
| 233 |
+
self.native_paths = native_paths
|
| 234 |
+
self.return_native = return_native
|
| 235 |
+
self.reference_metadata_dir = reference_metadata_dir
|
| 236 |
+
self.target_templates = target_templates
|
| 237 |
+
self.design_mask_templates = design_mask_templates
|
| 238 |
+
self.compute_affinity = compute_affinity
|
| 239 |
+
self.design = design
|
| 240 |
+
self.backbone_only = backbone_only
|
| 241 |
+
self.atom14 = atom14
|
| 242 |
+
self.max_seqs = max_seqs
|
| 243 |
+
self.inverse_fold = inverse_fold
|
| 244 |
+
self.extra_mol_dir = extra_mol_dir
|
| 245 |
+
self.extra_features = (
|
| 246 |
+
set(extra_features) if extra_features is not None else set()
|
| 247 |
+
)
|
| 248 |
+
self.disulfide_prob = disulfide_prob
|
| 249 |
+
self.disulfide_on = disulfide_on
|
| 250 |
+
self.design_mask_override = design_mask_override
|
| 251 |
+
self.use_new_design_mask = use_new_design_mask
|
| 252 |
+
self.multiplicity = multiplicity
|
| 253 |
+
self.return_designfolding = return_designfolding
|
| 254 |
+
|
| 255 |
+
def __getitem__(self, idx: int) -> Dict:
|
| 256 |
+
"""Get an item from the dataset.
|
| 257 |
+
|
| 258 |
+
Returns
|
| 259 |
+
-------
|
| 260 |
+
Dict[str, Tensor]
|
| 261 |
+
|
| 262 |
+
"""
|
| 263 |
+
data_sample_idx = idx // len(self.generated_paths)
|
| 264 |
+
idx = idx % len(self.generated_paths)
|
| 265 |
+
|
| 266 |
+
try:
|
| 267 |
+
feat = self.getitem_from_paths(
|
| 268 |
+
self.metadata_paths[idx],
|
| 269 |
+
self.generated_paths[idx],
|
| 270 |
+
self.native_paths[idx],
|
| 271 |
+
)
|
| 272 |
+
if self.multiplicity > 1:
|
| 273 |
+
feat["data_sample_idx"] = data_sample_idx
|
| 274 |
+
return feat
|
| 275 |
+
except DataFetchException:
|
| 276 |
+
idx = random.randint(0, len(self) - 1)
|
| 277 |
+
feat = self.getitem_from_paths(
|
| 278 |
+
self.metadata_paths[idx],
|
| 279 |
+
self.generated_paths[idx],
|
| 280 |
+
self.native_paths[idx],
|
| 281 |
+
)
|
| 282 |
+
if self.multiplicity > 1:
|
| 283 |
+
feat["data_sample_idx"] = data_sample_idx
|
| 284 |
+
return feat
|
| 285 |
+
|
| 286 |
+
def get_sample(self, design_dir: Path, sample_id: Optional[str] = None) -> Dict:
|
| 287 |
+
metadata_path = design_dir / f"{sample_id}.npz"
|
| 288 |
+
generated_path = design_dir / f"{sample_id}.cif"
|
| 289 |
+
native_path = design_dir / f"{sample_id}_native.cif"
|
| 290 |
+
return self.getitem_from_paths(metadata_path, generated_path, native_path)
|
| 291 |
+
|
| 292 |
+
def getitem_from_paths(self, metadata_path, generated_path, native_path) -> Dict:
|
| 293 |
+
"""Get an item from the dataset.
|
| 294 |
+
|
| 295 |
+
Returns
|
| 296 |
+
-------
|
| 297 |
+
Dict[str, Tensor]
|
| 298 |
+
|
| 299 |
+
"""
|
| 300 |
+
# Get metadata
|
| 301 |
+
|
| 302 |
+
if self.reference_metadata_dir:
|
| 303 |
+
reference_metadata_path = self.reference_metadata_dir / metadata_path.name
|
| 304 |
+
metadata = np.load(reference_metadata_path)
|
| 305 |
+
else:
|
| 306 |
+
metadata = np.load(metadata_path)
|
| 307 |
+
|
| 308 |
+
# get conditioning information from metadata
|
| 309 |
+
metadata_design_mask = metadata["design_mask"]
|
| 310 |
+
if self.use_new_design_mask:
|
| 311 |
+
design_mask = metadata["inverse_fold_design_mask"].astype(np.float32)
|
| 312 |
+
else:
|
| 313 |
+
design_mask = metadata_design_mask
|
| 314 |
+
|
| 315 |
+
ss_type = None
|
| 316 |
+
if "ss_type" in metadata:
|
| 317 |
+
ss_type = metadata["ss_type"]
|
| 318 |
+
|
| 319 |
+
binding_type = None
|
| 320 |
+
if "binding_type" in metadata:
|
| 321 |
+
binding_type = metadata["binding_type"]
|
| 322 |
+
|
| 323 |
+
# Per-residue amino acid constraints for inverse folding
|
| 324 |
+
aa_constraint_mask = None
|
| 325 |
+
if "aa_constraint_mask" in metadata:
|
| 326 |
+
loaded_mask = metadata["aa_constraint_mask"]
|
| 327 |
+
# Validate the loaded mask is a proper array with expected shape
|
| 328 |
+
if (
|
| 329 |
+
isinstance(loaded_mask, np.ndarray)
|
| 330 |
+
and loaded_mask.ndim == 2
|
| 331 |
+
and loaded_mask.shape[1] == 20 # 20 canonical amino acids
|
| 332 |
+
):
|
| 333 |
+
aa_constraint_mask = loaded_mask
|
| 334 |
+
else:
|
| 335 |
+
warnings.warn(
|
| 336 |
+
f"Invalid aa_constraint_mask in NPZ: "
|
| 337 |
+
f"type={type(loaded_mask)}, shape={getattr(loaded_mask, 'shape', 'N/A')}. "
|
| 338 |
+
f"Expected ndarray with shape (N, 20). Ignoring constraints.",
|
| 339 |
+
RuntimeWarning,
|
| 340 |
+
stacklevel=2,
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# Get features
|
| 344 |
+
feat = self.get_feat(generated_path, design_mask, ss_type, binding_type, aa_constraint_mask)
|
| 345 |
+
|
| 346 |
+
# Get native features
|
| 347 |
+
if self.return_native:
|
| 348 |
+
if "native_design_mask" in metadata.keys():
|
| 349 |
+
feat_native = self.get_feat(native_path, metadata["native_design_mask"])
|
| 350 |
+
else:
|
| 351 |
+
feat_native = self.get_feat(native_path, metadata_design_mask)
|
| 352 |
+
|
| 353 |
+
for k, v in feat_native.items():
|
| 354 |
+
feat[f"native_{k}"] = v
|
| 355 |
+
|
| 356 |
+
return feat
|
| 357 |
+
|
| 358 |
+
def get_feat(self, path, design_mask, ss_type=None, binding_type=None, aa_constraint_mask=None):
|
| 359 |
+
# Load design
|
| 360 |
+
if self.extra_mol_dir is not None:
|
| 361 |
+
mols = {
|
| 362 |
+
path.stem: pickle.load(path.open("rb"))
|
| 363 |
+
for path in self.extra_mol_dir.glob("*.pkl")
|
| 364 |
+
}
|
| 365 |
+
for mol_name, mol in mols.items():
|
| 366 |
+
element_counts = defaultdict(int)
|
| 367 |
+
for i, atom in enumerate(mol.GetAtoms()):
|
| 368 |
+
symbol = atom.GetSymbol()
|
| 369 |
+
element_counts[symbol] += 1
|
| 370 |
+
atom_name = f"{symbol}{element_counts[symbol]}"
|
| 371 |
+
atom.SetProp("name", atom_name)
|
| 372 |
+
|
| 373 |
+
try:
|
| 374 |
+
if path.suffix == ".cif":
|
| 375 |
+
structure = mmcif.parse_mmcif(
|
| 376 |
+
path, mols, moldir=self.moldir, use_original_res_idx=False
|
| 377 |
+
).data
|
| 378 |
+
elif path.suffix == ".pdb":
|
| 379 |
+
structure = parse_pdb(
|
| 380 |
+
path, moldir=self.moldir, use_original_res_idx=False
|
| 381 |
+
).data
|
| 382 |
+
else:
|
| 383 |
+
raise ValueError(f"Invalid path:{path}") # noqa: T201
|
| 384 |
+
except Exception as e: # noqa: BLE001
|
| 385 |
+
print(f"Failed to parse {path} with error {e}. Skipping.") # noqa: T201
|
| 386 |
+
raise DataFetchException() from e
|
| 387 |
+
|
| 388 |
+
# Tokenize structure
|
| 389 |
+
|
| 390 |
+
try:
|
| 391 |
+
tokenized = self.tokenizer.tokenize(
|
| 392 |
+
structure, inverse_fold=self.inverse_fold
|
| 393 |
+
)
|
| 394 |
+
except Exception as e: # noqa: BLE001
|
| 395 |
+
print(f"Tokenizer failed on {path} with error {e}. Skipping.") # noqa: T201
|
| 396 |
+
raise DataFetchException() from e
|
| 397 |
+
|
| 398 |
+
# Propagate design mask to obtain chain_design_mask (True whenever something is covalently bound to any residue that is in a chain that contains a design residue).
|
| 399 |
+
chain_design_mask = design_mask.astype(bool)
|
| 400 |
+
asym_id = tokenized.tokens["asym_id"]
|
| 401 |
+
while True:
|
| 402 |
+
design_chains = np.unique(asym_id[chain_design_mask])
|
| 403 |
+
chain_propagated = np.isin(asym_id, design_chains)
|
| 404 |
+
for i, j, _ in tokenized.bonds:
|
| 405 |
+
if any([chain_propagated[i], chain_propagated[j]]):
|
| 406 |
+
chain_propagated[i] = True
|
| 407 |
+
chain_propagated[j] = True
|
| 408 |
+
if np.equal(chain_propagated, chain_design_mask).all():
|
| 409 |
+
break
|
| 410 |
+
chain_design_mask = chain_propagated.astype(bool)
|
| 411 |
+
|
| 412 |
+
# Extract design for refolding the design only
|
| 413 |
+
if self.return_designfolding:
|
| 414 |
+
residue_design_mask = np.zeros(tokenized.token_to_res.max() + 1, dtype=bool)
|
| 415 |
+
np.put_along_axis(
|
| 416 |
+
residue_design_mask, tokenized.token_to_res, chain_design_mask, axis=0
|
| 417 |
+
)
|
| 418 |
+
structure = Structure.extract_residues(structure, residue_design_mask)
|
| 419 |
+
tokenized = self.tokenizer.tokenize(structure)
|
| 420 |
+
design_mask = design_mask[chain_design_mask]
|
| 421 |
+
chain_design_mask = chain_design_mask[chain_design_mask]
|
| 422 |
+
|
| 423 |
+
# For inverse folding, condition even on structure selected for design
|
| 424 |
+
if self.inverse_fold:
|
| 425 |
+
tokenized.tokens["structure_group"] = 1
|
| 426 |
+
|
| 427 |
+
try:
|
| 428 |
+
# Try to find molecules in the dataset moldir if provided
|
| 429 |
+
# Find missing ones in global moldir and check if all found
|
| 430 |
+
molecules = {}
|
| 431 |
+
molecules.update(self.canonicals)
|
| 432 |
+
mol_names = set(tokenized.tokens["res_name"].tolist())
|
| 433 |
+
mol_names = mol_names - set(self.canonicals.keys())
|
| 434 |
+
if mols is not None:
|
| 435 |
+
molecules.update(mols)
|
| 436 |
+
mol_names = mol_names - set(molecules.keys())
|
| 437 |
+
if self.moldir is not None:
|
| 438 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 439 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 440 |
+
except Exception as e: # noqa: BLE001
|
| 441 |
+
print(f"Molecule loading failed for {path} with error {e}. Skipping.")
|
| 442 |
+
raise DataFetchException() from e
|
| 443 |
+
|
| 444 |
+
# Set design mask for tokens. This will impact the featurization and add the atom14 features
|
| 445 |
+
if self.design:
|
| 446 |
+
tokenized.tokens["design_mask"] = torch.from_numpy(design_mask).bool()
|
| 447 |
+
|
| 448 |
+
# Finalize input data
|
| 449 |
+
input_data = Input(
|
| 450 |
+
tokens=tokenized.tokens,
|
| 451 |
+
bonds=tokenized.bonds,
|
| 452 |
+
token_to_res=tokenized.token_to_res,
|
| 453 |
+
structure=structure,
|
| 454 |
+
msa={},
|
| 455 |
+
templates=None,
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
# Compute features
|
| 459 |
+
try:
|
| 460 |
+
features = self.featurizer.process(
|
| 461 |
+
input_data,
|
| 462 |
+
molecules=molecules,
|
| 463 |
+
random=np.random.default_rng(None),
|
| 464 |
+
training=False,
|
| 465 |
+
max_seqs=self.max_seqs,
|
| 466 |
+
backbone_only=self.backbone_only,
|
| 467 |
+
atom14=self.atom14,
|
| 468 |
+
design=True,
|
| 469 |
+
compute_affinity=self.compute_affinity,
|
| 470 |
+
override_method="X-RAY DIFFRACTION",
|
| 471 |
+
disulfide_prob=self.disulfide_prob,
|
| 472 |
+
disulfide_on=self.disulfide_on,
|
| 473 |
+
)
|
| 474 |
+
except Exception as e: # noqa: BLE001
|
| 475 |
+
print(f"Featurizer failed on {path} with error {e}. Skipping.") # noqa: T201
|
| 476 |
+
raise DataFetchException() from e
|
| 477 |
+
|
| 478 |
+
# Set chain design mask
|
| 479 |
+
features["chain_design_mask"] = torch.from_numpy(chain_design_mask)
|
| 480 |
+
|
| 481 |
+
# Set conditioning variables that were set during design
|
| 482 |
+
if ss_type is not None:
|
| 483 |
+
features["ss_type"] = torch.from_numpy(ss_type).long()
|
| 484 |
+
if binding_type is not None:
|
| 485 |
+
features["binding_type"] = torch.from_numpy(binding_type).long()
|
| 486 |
+
# Per-residue amino acid constraints for inverse folding
|
| 487 |
+
if aa_constraint_mask is not None:
|
| 488 |
+
features["aa_constraint_mask"] = torch.from_numpy(aa_constraint_mask).float()
|
| 489 |
+
|
| 490 |
+
# If we do not want the design mask to impact the featurizer (e.g. represent atoms as atom14), we set the design mask only here.
|
| 491 |
+
if not self.design:
|
| 492 |
+
features["design_mask"] = torch.from_numpy(design_mask).bool()
|
| 493 |
+
|
| 494 |
+
# set chain_design_mask
|
| 495 |
+
# Override design mask for inverse folding if the part that should be inverse folded differs from the previously designed part.
|
| 496 |
+
if self.design and self.design_mask_override is not None:
|
| 497 |
+
msg = f"design mask being overridden with user input: {self.design_mask_override}"
|
| 498 |
+
print(msg)
|
| 499 |
+
new_design_mask = parse_redesign_yaml(
|
| 500 |
+
Path(self.design_mask_override), tokenized
|
| 501 |
+
)
|
| 502 |
+
features["inverse_fold_design_mask"] = torch.from_numpy(
|
| 503 |
+
new_design_mask
|
| 504 |
+
).bool()
|
| 505 |
+
|
| 506 |
+
# Perform assertions
|
| 507 |
+
if len(tokenized.tokens) != len(design_mask):
|
| 508 |
+
print(
|
| 509 |
+
f"WARNING: len(tokenized.tokens) [{len(tokenized.tokens)}] != len(design_mask) "
|
| 510 |
+
f"[{len(design_mask)}] for {path}"
|
| 511 |
+
)
|
| 512 |
+
features["exception"] = True
|
| 513 |
+
return features
|
| 514 |
+
else:
|
| 515 |
+
features["exception"] = False
|
| 516 |
+
|
| 517 |
+
# Set templates
|
| 518 |
+
if self.target_templates:
|
| 519 |
+
if self.design_mask_templates:
|
| 520 |
+
template_mask = ~features["design_mask"].numpy()
|
| 521 |
+
else:
|
| 522 |
+
template_mask = ~features["chain_design_mask"].numpy()
|
| 523 |
+
templates_features = template_from_tokens(tokenized, template_mask)
|
| 524 |
+
else:
|
| 525 |
+
# Compute template features
|
| 526 |
+
templates_features = load_dummy_templates(
|
| 527 |
+
tdim=1, num_tokens=len(features["res_type"])
|
| 528 |
+
)
|
| 529 |
+
features.update(templates_features)
|
| 530 |
+
|
| 531 |
+
features["affinity_token_mask"] = (
|
| 532 |
+
features["mol_type"] == const.chain_type_ids["NONPOLYMER"]
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
# Set additional features
|
| 536 |
+
features["str_gen"] = structure
|
| 537 |
+
features["path"] = path
|
| 538 |
+
features["id"] = path.stem
|
| 539 |
+
if "structure" in self.extra_features:
|
| 540 |
+
features["structure"] = structure
|
| 541 |
+
if "tokenized" in self.extra_features:
|
| 542 |
+
features["tokenized"] = tokenized
|
| 543 |
+
|
| 544 |
+
return features
|
| 545 |
+
|
| 546 |
+
def __len__(self) -> int:
|
| 547 |
+
return len(self.generated_paths) * self.multiplicity
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
class FromGeneratedDataModule(pl.LightningDataModule):
|
| 551 |
+
def __init__(
|
| 552 |
+
self,
|
| 553 |
+
cfg: DataConfig,
|
| 554 |
+
return_native: bool = False,
|
| 555 |
+
compute_affinity: bool = False,
|
| 556 |
+
target_templates: bool = False,
|
| 557 |
+
design_mask_templates: bool = False,
|
| 558 |
+
skip_existing: bool = False,
|
| 559 |
+
skip_existing_kind: str = None,
|
| 560 |
+
legacy_gen_suffix: str = "_gen.cif",
|
| 561 |
+
legacy_metadata_suffix: str = "_metadata.npz",
|
| 562 |
+
reference_metadata_dir: Optional[Path] = None,
|
| 563 |
+
design_dir: Optional[str] = None,
|
| 564 |
+
extra_features: Optional[List[str]] = None,
|
| 565 |
+
design_mask_override: Optional[str] = None,
|
| 566 |
+
subset_target_ids: Optional[str] = None,
|
| 567 |
+
skip_specific_ids: Optional[List[str]] = None,
|
| 568 |
+
use_new_design_mask: bool = False,
|
| 569 |
+
fail_if_no_designs: bool = False,
|
| 570 |
+
output_dir: Optional[str] = None,
|
| 571 |
+
) -> None:
|
| 572 |
+
super().__init__()
|
| 573 |
+
self.cfg = cfg
|
| 574 |
+
self.return_native = return_native
|
| 575 |
+
self.skip_existing = skip_existing
|
| 576 |
+
self.skip_existing_kind = skip_existing_kind
|
| 577 |
+
self.reference_metadata_dir = (
|
| 578 |
+
Path(reference_metadata_dir) if reference_metadata_dir else None
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
self.legacy_gen_suffix = legacy_gen_suffix
|
| 582 |
+
self.legacy_metadata_suffix = legacy_metadata_suffix
|
| 583 |
+
self.compute_affinity = compute_affinity
|
| 584 |
+
self.target_templates = target_templates
|
| 585 |
+
self.design_mask_templates = design_mask_templates
|
| 586 |
+
self.extra_features = extra_features
|
| 587 |
+
self.disulfide_prob = cfg.disulfide_prob
|
| 588 |
+
self.disulfide_on = cfg.disulfide_on
|
| 589 |
+
self.design_mask_override = cfg.design_mask_override
|
| 590 |
+
self.collate = collate
|
| 591 |
+
self.fail_if_no_designs = fail_if_no_designs
|
| 592 |
+
self.subset_target_ids = subset_target_ids
|
| 593 |
+
self.output_dir = Path(output_dir) if output_dir else None
|
| 594 |
+
|
| 595 |
+
if design_dir is not None:
|
| 596 |
+
self.init_dataset(
|
| 597 |
+
design_dir,
|
| 598 |
+
skip_specific_ids=skip_specific_ids,
|
| 599 |
+
extra_features=extra_features,
|
| 600 |
+
use_new_design_mask=use_new_design_mask,
|
| 601 |
+
)
|
| 602 |
+
else:
|
| 603 |
+
# Load canonical molecules
|
| 604 |
+
canonicals = load_canonicals(self.cfg.moldir)
|
| 605 |
+
|
| 606 |
+
self.predict_set = FromGeneratedDataset(
|
| 607 |
+
generated_paths=[],
|
| 608 |
+
metadata_paths=[],
|
| 609 |
+
native_paths=[],
|
| 610 |
+
canonicals=canonicals,
|
| 611 |
+
moldir=Path(self.cfg.moldir),
|
| 612 |
+
tokenizer=self.cfg.tokenizer,
|
| 613 |
+
featurizer=self.cfg.featurizer,
|
| 614 |
+
return_native=self.return_native,
|
| 615 |
+
reference_metadata_dir=self.reference_metadata_dir,
|
| 616 |
+
target_templates=self.target_templates,
|
| 617 |
+
design_mask_templates=self.design_mask_templates,
|
| 618 |
+
compute_affinity=self.compute_affinity,
|
| 619 |
+
design=self.cfg.design,
|
| 620 |
+
backbone_only=self.cfg.backbone_only,
|
| 621 |
+
atom14=self.cfg.atom14,
|
| 622 |
+
max_seqs=self.cfg.max_seqs,
|
| 623 |
+
inverse_fold=self.cfg.inverse_fold,
|
| 624 |
+
extra_features=self.extra_features,
|
| 625 |
+
disulfide_prob=self.disulfide_prob,
|
| 626 |
+
disulfide_on=self.disulfide_on,
|
| 627 |
+
design_mask_override=self.design_mask_override,
|
| 628 |
+
use_new_design_mask=use_new_design_mask,
|
| 629 |
+
multiplicity=self.cfg.multiplicity,
|
| 630 |
+
return_designfolding=self.cfg.return_designfolding,
|
| 631 |
+
)
|
| 632 |
+
|
| 633 |
+
def init_dataset(
|
| 634 |
+
self,
|
| 635 |
+
design_dir,
|
| 636 |
+
skip_specific_ids: Optional[List[str]] = None,
|
| 637 |
+
extra_features: Optional[List[str]] = None,
|
| 638 |
+
use_new_design_mask: bool = False,
|
| 639 |
+
):
|
| 640 |
+
print(f"Initializing FromGeneratedDataModule datasets for {design_dir}")
|
| 641 |
+
design_dir = Path(design_dir)
|
| 642 |
+
assert design_dir.exists(), f"Path does not exist design_dir: {design_dir}"
|
| 643 |
+
|
| 644 |
+
# Aggregate generated structure files (.cif or .pdb) while skipping companion native/metadata files.
|
| 645 |
+
generated_paths = sorted(
|
| 646 |
+
p
|
| 647 |
+
for p in design_dir.iterdir()
|
| 648 |
+
if p.suffix in {".cif", ".pdb"}
|
| 649 |
+
and "_native.cif" not in p.name
|
| 650 |
+
and "_metadata.npz" not in p.name
|
| 651 |
+
)
|
| 652 |
+
if self.fail_if_no_designs and len(generated_paths) == 0:
|
| 653 |
+
raise ValueError(f"No designs found in {design_dir}")
|
| 654 |
+
|
| 655 |
+
# skip certain ids
|
| 656 |
+
num_files_before = len(generated_paths)
|
| 657 |
+
print(
|
| 658 |
+
f"[Info] Number of files to process (including already processed ones): {num_files_before}"
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
if skip_specific_ids:
|
| 662 |
+
filtered_generated_paths = [
|
| 663 |
+
p
|
| 664 |
+
for p in generated_paths
|
| 665 |
+
if not any(prob_id in p.name for prob_id in skip_specific_ids)
|
| 666 |
+
]
|
| 667 |
+
num_files_after = len(filtered_generated_paths)
|
| 668 |
+
print(f"[Info] Skipped specific IDs: {skip_specific_ids}")
|
| 669 |
+
print(f"[Info] Number of files after filtering: {num_files_after}")
|
| 670 |
+
generated_paths = filtered_generated_paths
|
| 671 |
+
|
| 672 |
+
if self.skip_existing:
|
| 673 |
+
# Functions to map an input path to a list of output paths.
|
| 674 |
+
# If all output paths exist, the input path is skipped.
|
| 675 |
+
def output_path_inverse_fold(input_path):
|
| 676 |
+
assert self.output_dir is not None
|
| 677 |
+
return [
|
| 678 |
+
self.output_dir / f"{input_path.stem}.cif",
|
| 679 |
+
self.output_dir / f"{input_path.stem}.npz",
|
| 680 |
+
]
|
| 681 |
+
|
| 682 |
+
def output_path_folded(input_path):
|
| 683 |
+
output_dir = (
|
| 684 |
+
design_dir / const.folding_dirname
|
| 685 |
+
if self.output_dir is None
|
| 686 |
+
else self.output_dir
|
| 687 |
+
)
|
| 688 |
+
return [
|
| 689 |
+
output_dir / f"{input_path.stem}.npz",
|
| 690 |
+
output_dir / f"{input_path.stem}.npz",
|
| 691 |
+
]
|
| 692 |
+
|
| 693 |
+
def output_path_design_folded(input_path):
|
| 694 |
+
output_dir = (
|
| 695 |
+
design_dir / const.refold_design_cif_dirname
|
| 696 |
+
if self.output_dir is None
|
| 697 |
+
else self.output_dir
|
| 698 |
+
)
|
| 699 |
+
return [
|
| 700 |
+
output_dir / f"{input_path.stem}.cif",
|
| 701 |
+
]
|
| 702 |
+
|
| 703 |
+
def output_path_affinity(input_path):
|
| 704 |
+
output_dir = (
|
| 705 |
+
design_dir / const.affinity_dirname
|
| 706 |
+
if self.output_dir is None
|
| 707 |
+
else self.output_dir
|
| 708 |
+
)
|
| 709 |
+
return [
|
| 710 |
+
output_dir / f"{input_path.stem}.npz",
|
| 711 |
+
]
|
| 712 |
+
|
| 713 |
+
def output_path_analyzed(input_path):
|
| 714 |
+
output_dir = (
|
| 715 |
+
design_dir / const.metrics_dirname
|
| 716 |
+
if self.output_dir is None
|
| 717 |
+
else self.output_dir
|
| 718 |
+
)
|
| 719 |
+
return [
|
| 720 |
+
output_dir / f"data_{input_path.stem}.npz",
|
| 721 |
+
output_dir / f"metrics_{input_path.stem}.npz",
|
| 722 |
+
]
|
| 723 |
+
|
| 724 |
+
mappings = {
|
| 725 |
+
"inverse_fold": output_path_inverse_fold,
|
| 726 |
+
"folded": output_path_folded,
|
| 727 |
+
"design_folded": output_path_design_folded,
|
| 728 |
+
"affinity": output_path_affinity,
|
| 729 |
+
"analyzed": output_path_analyzed,
|
| 730 |
+
}
|
| 731 |
+
if self.skip_existing_kind not in mappings:
|
| 732 |
+
msg = f"Invalid skip_existing_kind: {self.skip_existing_kind}. Available kinds: {list(mappings.keys())}"
|
| 733 |
+
raise ValueError(msg)
|
| 734 |
+
selected_mapping = mappings[self.skip_existing_kind]
|
| 735 |
+
|
| 736 |
+
generated_paths = [
|
| 737 |
+
p
|
| 738 |
+
for p in generated_paths
|
| 739 |
+
if not all(output_path.exists() for output_path in selected_mapping(p))
|
| 740 |
+
]
|
| 741 |
+
msg = f"[Info] Skipped already {self.skip_existing_kind} IDs. Number of files after filtering: {len(generated_paths)}"
|
| 742 |
+
print(msg)
|
| 743 |
+
|
| 744 |
+
target_ids = [
|
| 745 |
+
re.search(rf"{self.cfg.target_id_regex}", p.stem).group(1)
|
| 746 |
+
for p in generated_paths
|
| 747 |
+
]
|
| 748 |
+
target_ids = list(set(target_ids))
|
| 749 |
+
|
| 750 |
+
if self.cfg.num_targets is not None:
|
| 751 |
+
target_ids = target_ids[: self.cfg.num_targets]
|
| 752 |
+
generated_paths = [
|
| 753 |
+
p
|
| 754 |
+
for p in generated_paths
|
| 755 |
+
if re.search(rf"{self.cfg.target_id_regex}", p.stem).group(1)
|
| 756 |
+
in target_ids
|
| 757 |
+
]
|
| 758 |
+
|
| 759 |
+
filtered_paths = []
|
| 760 |
+
for target_id in target_ids:
|
| 761 |
+
paths_of_target = [
|
| 762 |
+
p
|
| 763 |
+
for p in generated_paths
|
| 764 |
+
if re.search(rf"{self.cfg.target_id_regex}", p.stem).group(1)
|
| 765 |
+
== target_id
|
| 766 |
+
]
|
| 767 |
+
|
| 768 |
+
filtered_paths.extend(paths_of_target[: self.cfg.samples_per_target])
|
| 769 |
+
|
| 770 |
+
filtered_paths2 = []
|
| 771 |
+
if self.subset_target_ids is not None:
|
| 772 |
+
subset_ids = [
|
| 773 |
+
l.strip() for l in open(self.subset_target_ids, "r").readlines()
|
| 774 |
+
]
|
| 775 |
+
for path in filtered_paths:
|
| 776 |
+
if any([sid in str(path) for sid in subset_ids]):
|
| 777 |
+
filtered_paths2.append(path)
|
| 778 |
+
filtered_paths = filtered_paths2
|
| 779 |
+
|
| 780 |
+
metadata_paths = []
|
| 781 |
+
native_paths = []
|
| 782 |
+
# Sort the paths to make sure each subprocess (when using multiple GPUs) has the same order and the index distribution when fetching from the dataset fetches the correct paths instead of fetching the same paths multiple times.
|
| 783 |
+
filtered_paths = sorted(filtered_paths)
|
| 784 |
+
for path in filtered_paths:
|
| 785 |
+
ext = path.suffix
|
| 786 |
+
|
| 787 |
+
# Legacy files contain "_gen" before the extension.
|
| 788 |
+
if path.stem.endswith("_gen"):
|
| 789 |
+
metadata_path = path.with_name(
|
| 790 |
+
path.name.replace(f"_gen{ext}", "_metadata.npz")
|
| 791 |
+
)
|
| 792 |
+
native_path = path.with_name(
|
| 793 |
+
path.name.replace(f"_gen{ext}", "_native.cif")
|
| 794 |
+
)
|
| 795 |
+
else:
|
| 796 |
+
metadata_path = path.with_suffix(".npz")
|
| 797 |
+
native_path = path.with_name(f"{path.stem}_native.cif")
|
| 798 |
+
|
| 799 |
+
if not metadata_path.exists():
|
| 800 |
+
print(f"[WARNING] Path does not exist: {metadata_path}")
|
| 801 |
+
metadata_paths.append(metadata_path)
|
| 802 |
+
if self.return_native:
|
| 803 |
+
if not native_path.exists():
|
| 804 |
+
print(f"[WARNING] Path does not exist: {native_path}")
|
| 805 |
+
native_paths.append(native_path)
|
| 806 |
+
else:
|
| 807 |
+
native_paths.append(None)
|
| 808 |
+
msg = f"Found {len(target_ids)} targets and {len(filtered_paths)} remaining designs that still need to be processed in this step."
|
| 809 |
+
print(msg)
|
| 810 |
+
|
| 811 |
+
# Load canonical molecules
|
| 812 |
+
canonicals = load_canonicals(self.cfg.moldir)
|
| 813 |
+
|
| 814 |
+
self.predict_set = FromGeneratedDataset(
|
| 815 |
+
generated_paths=filtered_paths,
|
| 816 |
+
metadata_paths=metadata_paths,
|
| 817 |
+
native_paths=native_paths,
|
| 818 |
+
canonicals=canonicals,
|
| 819 |
+
moldir=Path(self.cfg.moldir),
|
| 820 |
+
tokenizer=self.cfg.tokenizer,
|
| 821 |
+
featurizer=self.cfg.featurizer,
|
| 822 |
+
return_native=self.return_native,
|
| 823 |
+
reference_metadata_dir=self.reference_metadata_dir,
|
| 824 |
+
target_templates=self.target_templates,
|
| 825 |
+
design_mask_templates=self.design_mask_templates,
|
| 826 |
+
compute_affinity=self.compute_affinity,
|
| 827 |
+
design=self.cfg.design,
|
| 828 |
+
backbone_only=self.cfg.backbone_only,
|
| 829 |
+
atom14=self.cfg.atom14,
|
| 830 |
+
max_seqs=self.cfg.max_seqs,
|
| 831 |
+
inverse_fold=self.cfg.inverse_fold,
|
| 832 |
+
extra_mol_dir=design_dir / const.molecules_dirname,
|
| 833 |
+
extra_features=self.extra_features,
|
| 834 |
+
disulfide_prob=self.disulfide_prob,
|
| 835 |
+
disulfide_on=self.disulfide_on,
|
| 836 |
+
design_mask_override=self.design_mask_override,
|
| 837 |
+
use_new_design_mask=use_new_design_mask,
|
| 838 |
+
multiplicity=self.cfg.multiplicity,
|
| 839 |
+
return_designfolding=self.cfg.return_designfolding,
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
def predict_dataloader(self) -> DataLoader:
|
| 843 |
+
return DataLoader(
|
| 844 |
+
self.predict_set,
|
| 845 |
+
batch_size=self.cfg.batch_size,
|
| 846 |
+
num_workers=self.cfg.num_workers,
|
| 847 |
+
pin_memory=self.cfg.pin_memory,
|
| 848 |
+
shuffle=False,
|
| 849 |
+
collate_fn=collate,
|
| 850 |
+
)
|
| 851 |
+
|
| 852 |
+
def transfer_batch_to_device(
|
| 853 |
+
self,
|
| 854 |
+
batch: Dict,
|
| 855 |
+
device: torch.device,
|
| 856 |
+
dataloader_idx: int = 0,
|
| 857 |
+
) -> Dict:
|
| 858 |
+
for key in batch:
|
| 859 |
+
if key not in [
|
| 860 |
+
"metadata",
|
| 861 |
+
"str_gen",
|
| 862 |
+
"id",
|
| 863 |
+
"path",
|
| 864 |
+
"native_metadata",
|
| 865 |
+
"native_str_gen",
|
| 866 |
+
"native_id",
|
| 867 |
+
"native_path",
|
| 868 |
+
"exception",
|
| 869 |
+
"native_exception",
|
| 870 |
+
"skip",
|
| 871 |
+
"native_skip",
|
| 872 |
+
"structure_bonds",
|
| 873 |
+
"native_structure_bonds",
|
| 874 |
+
"extra_mols",
|
| 875 |
+
"native_extra_mols",
|
| 876 |
+
"structure",
|
| 877 |
+
"tokenized",
|
| 878 |
+
"data_sample_idx",
|
| 879 |
+
]:
|
| 880 |
+
batch[key] = batch[key].to(device)
|
| 881 |
+
|
| 882 |
+
return batch
|
model/boltzgen/task/predict/data_from_yaml.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import re
|
| 4 |
+
from typing import Dict, List, Optional, Union
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pytorch_lightning as pl
|
| 8 |
+
import torch
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
from torch.utils.data import DataLoader
|
| 11 |
+
from rdkit.Chem import Mol
|
| 12 |
+
from boltzgen.data import const
|
| 13 |
+
from boltzgen.data.data import Input
|
| 14 |
+
from boltzgen.data.feature.featurizer import Featurizer
|
| 15 |
+
from boltzgen.data.pad import pad_to_max
|
| 16 |
+
from boltzgen.data.mol import load_canonicals, load_molecules
|
| 17 |
+
from boltzgen.data.parse.schema import YamlDesignParser
|
| 18 |
+
from boltzgen.data.template.features import load_dummy_templates
|
| 19 |
+
from boltzgen.data.tokenize.tokenizer import Tokenizer
|
| 20 |
+
from boltzgen.data.data import Input
|
| 21 |
+
from boltzgen.data.select.protein import ProteinSelector
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class DataConfig:
|
| 26 |
+
"""Data configuration."""
|
| 27 |
+
|
| 28 |
+
moldir: str
|
| 29 |
+
multiplicity: int
|
| 30 |
+
yaml_path: Union[List[str], str]
|
| 31 |
+
tokenizer: Tokenizer
|
| 32 |
+
featurizer: Featurizer
|
| 33 |
+
backbone_only: bool = False
|
| 34 |
+
atom14: bool = False
|
| 35 |
+
atom37: bool = False
|
| 36 |
+
design: bool = True
|
| 37 |
+
compute_affinity: bool = False
|
| 38 |
+
disulfide_prob: float = 1.0
|
| 39 |
+
disulfide_on: bool = False
|
| 40 |
+
skip_existing: bool = False
|
| 41 |
+
skip_offset: int = 0
|
| 42 |
+
diffusion_samples: int = 1
|
| 43 |
+
output_dir: Optional[str] = None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass
|
| 49 |
+
class Dataset:
|
| 50 |
+
yaml_path: Union[List[str], str]
|
| 51 |
+
tokenizer: Tokenizer
|
| 52 |
+
featurizer: Featurizer
|
| 53 |
+
multiplicity: int = 1
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def collate(data: List[Dict[str, Tensor]]) -> Dict[str, Tensor]:
|
| 57 |
+
"""Collate the data.
|
| 58 |
+
|
| 59 |
+
Parameters
|
| 60 |
+
----------
|
| 61 |
+
data : List[Dict[str, Tensor]]
|
| 62 |
+
The data to collate.
|
| 63 |
+
|
| 64 |
+
Returns
|
| 65 |
+
-------
|
| 66 |
+
Dict[str, Tensor]
|
| 67 |
+
The collated data.
|
| 68 |
+
|
| 69 |
+
"""
|
| 70 |
+
# Get the keys
|
| 71 |
+
keys = data[0].keys()
|
| 72 |
+
|
| 73 |
+
# Collate the data
|
| 74 |
+
collated = {}
|
| 75 |
+
for key in keys:
|
| 76 |
+
values = [d[key] for d in data]
|
| 77 |
+
|
| 78 |
+
if key not in [
|
| 79 |
+
"all_coords",
|
| 80 |
+
"all_resolved_mask",
|
| 81 |
+
"crop_to_all_atom_map",
|
| 82 |
+
"chain_symmetries",
|
| 83 |
+
"amino_acids_symmetries",
|
| 84 |
+
"ligand_symmetries",
|
| 85 |
+
"activity_name",
|
| 86 |
+
"activity_qualifier",
|
| 87 |
+
"sid",
|
| 88 |
+
"cid",
|
| 89 |
+
"aid",
|
| 90 |
+
"normalized_protein_accession",
|
| 91 |
+
"pair_id",
|
| 92 |
+
"record",
|
| 93 |
+
"id",
|
| 94 |
+
"structure",
|
| 95 |
+
"tokenized",
|
| 96 |
+
"structure_bonds",
|
| 97 |
+
"extra_mols",
|
| 98 |
+
"data_sample_idx",
|
| 99 |
+
]:
|
| 100 |
+
# Check if all have the same shape
|
| 101 |
+
shape = values[0].shape
|
| 102 |
+
if not all(v.shape == shape for v in values):
|
| 103 |
+
values = pad_to_max(values, 0)
|
| 104 |
+
else:
|
| 105 |
+
values = torch.stack(values, dim=0)
|
| 106 |
+
|
| 107 |
+
# Stack the values
|
| 108 |
+
collated[key] = values
|
| 109 |
+
|
| 110 |
+
return collated
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class PredictionDataset(torch.utils.data.Dataset):
|
| 114 |
+
"""Base iterable dataset."""
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
dataset: Dataset,
|
| 119 |
+
canonicals: dict[str, Mol],
|
| 120 |
+
moldir: str,
|
| 121 |
+
backbone_only: bool = False,
|
| 122 |
+
atom14: bool = False,
|
| 123 |
+
atom37: bool = False,
|
| 124 |
+
extra_features: Optional[List[str]] = None,
|
| 125 |
+
design: bool = True,
|
| 126 |
+
compute_affinity: bool = False,
|
| 127 |
+
disulfide_prob: float = 1.0,
|
| 128 |
+
disulfide_on: bool = False,
|
| 129 |
+
skip_offset: int = 0,
|
| 130 |
+
) -> None:
|
| 131 |
+
"""Initialize the training dataset.
|
| 132 |
+
|
| 133 |
+
Parameters
|
| 134 |
+
----------
|
| 135 |
+
datasets : List[Dataset]
|
| 136 |
+
The datasets to sample from.
|
| 137 |
+
|
| 138 |
+
"""
|
| 139 |
+
super().__init__()
|
| 140 |
+
self.dataset = dataset
|
| 141 |
+
self.moldir = moldir
|
| 142 |
+
self.canonicals = canonicals
|
| 143 |
+
self.backbone_only = backbone_only
|
| 144 |
+
self.atom14 = atom14
|
| 145 |
+
self.atom37 = atom37
|
| 146 |
+
self.skip_offset = skip_offset
|
| 147 |
+
path = dataset.yaml_path
|
| 148 |
+
self.yaml_paths = [path] if isinstance(path, str) else path
|
| 149 |
+
|
| 150 |
+
for path in self.yaml_paths:
|
| 151 |
+
filename = Path(path).name
|
| 152 |
+
if re.search(r"_\d+\.yaml$", filename):
|
| 153 |
+
raise ValueError(
|
| 154 |
+
f"Illegal YAML filename for '{str(path)}': names must not end with the pattern _\\d+\\.yaml so, e.g., the ends '_001.yaml' or '_4.yaml' are not allowed."
|
| 155 |
+
"This pattern is reserved for internal file indexing. Sorry :)"
|
| 156 |
+
)
|
| 157 |
+
if "_native" in filename:
|
| 158 |
+
raise ValueError(
|
| 159 |
+
f"Illegal YAML filename for '{str(path)}': names must not contain '_native' because this substring is reserved for native structure companions. Sorry :)"
|
| 160 |
+
)
|
| 161 |
+
self.extra_features = (
|
| 162 |
+
set(extra_features) if extra_features is not None else set()
|
| 163 |
+
)
|
| 164 |
+
self.selector = (
|
| 165 |
+
ProteinSelector(
|
| 166 |
+
design_neighborhood_sizes=[2, 4, 6, 8, 10, 12, 14, 16, 18],
|
| 167 |
+
substructure_neighborhood_sizes=[2, 4, 6, 8, 10, 12, 24],
|
| 168 |
+
structure_condition_prob=1.0,
|
| 169 |
+
distance_noise_std=1,
|
| 170 |
+
run_selection=True,
|
| 171 |
+
specify_binding_sites=True,
|
| 172 |
+
ss_condition_prob=0.1,
|
| 173 |
+
select_all=False,
|
| 174 |
+
chain_reindexing=False,
|
| 175 |
+
)
|
| 176 |
+
)
|
| 177 |
+
self.design = design
|
| 178 |
+
self.compute_affinity = compute_affinity
|
| 179 |
+
self.disulfide_prob = disulfide_prob
|
| 180 |
+
self.disulfide_on = disulfide_on
|
| 181 |
+
|
| 182 |
+
self.mols = {}
|
| 183 |
+
self.parser = YamlDesignParser(mol_dir=self.moldir)
|
| 184 |
+
|
| 185 |
+
def __getitem__(self, idx: int) -> Dict:
|
| 186 |
+
"""Get an item from the dataset.
|
| 187 |
+
|
| 188 |
+
Returns
|
| 189 |
+
-------
|
| 190 |
+
Dict[str, Tensor]
|
| 191 |
+
The sampled data features.
|
| 192 |
+
|
| 193 |
+
"""
|
| 194 |
+
path = Path(self.yaml_paths[idx % len(self.yaml_paths)])
|
| 195 |
+
feat = self.get_sample(path)
|
| 196 |
+
data_sample_idx = idx // len(self.yaml_paths) + self.skip_offset
|
| 197 |
+
if self.dataset.multiplicity > 1:
|
| 198 |
+
feat["data_sample_idx"] = data_sample_idx
|
| 199 |
+
return feat
|
| 200 |
+
|
| 201 |
+
def get_sample(self, path: Path, sample_id: Optional[str] = None) -> Dict:
|
| 202 |
+
# Get itemn also needs to take a smaple id as input
|
| 203 |
+
parsed = self.parser.parse_yaml(
|
| 204 |
+
path, mol_dir=self.moldir, mols=self.mols
|
| 205 |
+
)
|
| 206 |
+
structure = parsed.structure
|
| 207 |
+
design_info = parsed.design_info
|
| 208 |
+
|
| 209 |
+
# Tokenize structure
|
| 210 |
+
tokenized = self.dataset.tokenizer.tokenize(structure)
|
| 211 |
+
|
| 212 |
+
# Transfer conditioning information that is stored in tokens
|
| 213 |
+
token_to_res = tokenized.token_to_res
|
| 214 |
+
tokenized.tokens["design_mask"] = design_info.res_design_mask[token_to_res]
|
| 215 |
+
tokenized.tokens["binding_type"] = design_info.res_binding_type[token_to_res]
|
| 216 |
+
tokenized.tokens["structure_group"] = design_info.res_structure_groups[
|
| 217 |
+
token_to_res
|
| 218 |
+
]
|
| 219 |
+
# Transfer per-residue amino acid constraints (shape: num_tokens x 20)
|
| 220 |
+
tokenized.tokens["aa_constraint_mask"] = design_info.res_aa_constraint_mask[
|
| 221 |
+
token_to_res
|
| 222 |
+
]
|
| 223 |
+
|
| 224 |
+
# Propagate design mask to obtain chain_design_mask (True whenever something is covalently bound to any residue that is in a chain that contains a design residue).
|
| 225 |
+
chain_design_mask = tokenized.tokens["design_mask"].astype(bool)
|
| 226 |
+
asym_id = tokenized.tokens["asym_id"]
|
| 227 |
+
while True:
|
| 228 |
+
design_chains = np.unique(asym_id[chain_design_mask])
|
| 229 |
+
chain_propagated = np.isin(asym_id, design_chains)
|
| 230 |
+
for i, j, _ in tokenized.bonds:
|
| 231 |
+
if any([chain_propagated[i], chain_propagated[j]]):
|
| 232 |
+
chain_propagated[i] = True
|
| 233 |
+
chain_propagated[j] = True
|
| 234 |
+
if np.equal(chain_propagated, chain_design_mask).all():
|
| 235 |
+
break
|
| 236 |
+
chain_design_mask = chain_propagated.astype(bool)
|
| 237 |
+
|
| 238 |
+
# Try to find molecules in the dataset moldir if provided
|
| 239 |
+
# Find missing ones in global moldir and check if all found
|
| 240 |
+
molecules = {}
|
| 241 |
+
molecules.update(self.canonicals)
|
| 242 |
+
mol_names = set(tokenized.tokens["res_name"].tolist())
|
| 243 |
+
mol_names = mol_names - set(self.canonicals.keys())
|
| 244 |
+
mol_names = mol_names - set(parsed.extra_mols.keys())
|
| 245 |
+
if self.moldir is not None:
|
| 246 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 247 |
+
|
| 248 |
+
mol_names = mol_names - set(molecules.keys())
|
| 249 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 250 |
+
molecules.update(parsed.extra_mols)
|
| 251 |
+
|
| 252 |
+
# Finalize input data
|
| 253 |
+
input_data = Input(
|
| 254 |
+
tokens=tokenized.tokens,
|
| 255 |
+
bonds=tokenized.bonds,
|
| 256 |
+
token_to_res=token_to_res,
|
| 257 |
+
structure=structure,
|
| 258 |
+
msa={},
|
| 259 |
+
templates=None,
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
# Compute features
|
| 263 |
+
features = self.dataset.featurizer.process(
|
| 264 |
+
input_data,
|
| 265 |
+
molecules=molecules,
|
| 266 |
+
random=np.random.default_rng(None),
|
| 267 |
+
training=False,
|
| 268 |
+
max_seqs=1,
|
| 269 |
+
backbone_only=self.backbone_only,
|
| 270 |
+
atom14=self.atom14,
|
| 271 |
+
atom37=self.atom37,
|
| 272 |
+
design=self.design,
|
| 273 |
+
override_method="X-RAY DIFFRACTION",
|
| 274 |
+
compute_affinity=self.compute_affinity,
|
| 275 |
+
disulfide_prob=self.disulfide_prob,
|
| 276 |
+
disulfide_on=self.disulfide_on,
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# transfer secondary structure conditioning
|
| 280 |
+
ss_type = design_info.res_ss_types[token_to_res]
|
| 281 |
+
features["ss_type"] = torch.from_numpy(ss_type).to(features["ss_type"])
|
| 282 |
+
features["design_ss_mask"][ss_type != const.ss_type_ids["UNSPECIFIED"]] = 1
|
| 283 |
+
|
| 284 |
+
# set chain_design_mask
|
| 285 |
+
features["chain_design_mask"] = torch.from_numpy(chain_design_mask)
|
| 286 |
+
|
| 287 |
+
# Compute template features
|
| 288 |
+
templates_features = load_dummy_templates(
|
| 289 |
+
tdim=1, num_tokens=len(features["res_type"])
|
| 290 |
+
)
|
| 291 |
+
features.update(templates_features)
|
| 292 |
+
|
| 293 |
+
# set last necessary features
|
| 294 |
+
features["idx_dataset"] = torch.tensor(1)
|
| 295 |
+
|
| 296 |
+
# If a smaple id is provided then this should be the sample id instead of path.stem
|
| 297 |
+
if sample_id is not None:
|
| 298 |
+
features["id"] = sample_id
|
| 299 |
+
else:
|
| 300 |
+
features["id"] = path.stem
|
| 301 |
+
if "structure" in self.extra_features:
|
| 302 |
+
features["structure"] = structure
|
| 303 |
+
if "tokenized" in self.extra_features:
|
| 304 |
+
features["tokenized"] = tokenized
|
| 305 |
+
|
| 306 |
+
return features
|
| 307 |
+
|
| 308 |
+
def __len__(self) -> int:
|
| 309 |
+
"""Get the length of the dataset.
|
| 310 |
+
|
| 311 |
+
Returns
|
| 312 |
+
-------
|
| 313 |
+
int
|
| 314 |
+
The length of the dataset.
|
| 315 |
+
|
| 316 |
+
"""
|
| 317 |
+
total = len(self.yaml_paths) * (self.dataset.multiplicity - self.skip_offset)
|
| 318 |
+
return max(total, 0)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
class FromYamlDataModule(pl.LightningDataModule):
|
| 322 |
+
"""DataModule for BoltzGen."""
|
| 323 |
+
|
| 324 |
+
def __init__(
|
| 325 |
+
self, cfg: DataConfig, batch_size, num_workers, pin_memory, extra_features=None
|
| 326 |
+
) -> None:
|
| 327 |
+
"""Initialize the DataModule.
|
| 328 |
+
|
| 329 |
+
Parameters
|
| 330 |
+
----------
|
| 331 |
+
config : DataConfig
|
| 332 |
+
The data configuration.
|
| 333 |
+
|
| 334 |
+
"""
|
| 335 |
+
super().__init__()
|
| 336 |
+
|
| 337 |
+
if cfg.skip_existing and cfg.output_dir is not None:
|
| 338 |
+
design_dir = Path(cfg.output_dir)
|
| 339 |
+
max_idx: int = -1
|
| 340 |
+
if design_dir.exists():
|
| 341 |
+
pattern = re.compile(r"_(\d+)(?:\.[^.]+)$")
|
| 342 |
+
max_idx = max(
|
| 343 |
+
(
|
| 344 |
+
int(m.group(1))
|
| 345 |
+
for fp in design_dir.iterdir()
|
| 346 |
+
if fp.suffix in {".cif", ".pdb"}
|
| 347 |
+
and not any(s in fp.name for s in ("_native.cif", "_metadata.npz"))
|
| 348 |
+
for m in [pattern.search(fp.name)]
|
| 349 |
+
if m
|
| 350 |
+
),
|
| 351 |
+
default=-1,
|
| 352 |
+
)
|
| 353 |
+
n_samples = getattr(cfg, "diffusion_samples", 1)
|
| 354 |
+
cfg.skip_offset = (max_idx // max(n_samples, 1)) + 1 if max_idx >= 0 else 0
|
| 355 |
+
|
| 356 |
+
self.cfg = cfg
|
| 357 |
+
self.batch_size = batch_size
|
| 358 |
+
self.num_workers = num_workers
|
| 359 |
+
self.pin_memory = pin_memory
|
| 360 |
+
self.collate = collate
|
| 361 |
+
|
| 362 |
+
dataset = Dataset(
|
| 363 |
+
yaml_path=cfg.yaml_path,
|
| 364 |
+
multiplicity=cfg.multiplicity,
|
| 365 |
+
tokenizer=cfg.tokenizer,
|
| 366 |
+
featurizer=cfg.featurizer,
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
# Load canonical molecules
|
| 370 |
+
canonicals = load_canonicals(cfg.moldir)
|
| 371 |
+
|
| 372 |
+
self.predict_set = PredictionDataset(
|
| 373 |
+
dataset=dataset,
|
| 374 |
+
canonicals=canonicals,
|
| 375 |
+
moldir=Path(cfg.moldir),
|
| 376 |
+
backbone_only=cfg.backbone_only,
|
| 377 |
+
atom14=cfg.atom14,
|
| 378 |
+
atom37=cfg.atom37,
|
| 379 |
+
extra_features=extra_features,
|
| 380 |
+
design=cfg.design,
|
| 381 |
+
compute_affinity=cfg.compute_affinity,
|
| 382 |
+
disulfide_prob=cfg.disulfide_prob,
|
| 383 |
+
disulfide_on=cfg.disulfide_on,
|
| 384 |
+
skip_offset=cfg.skip_offset,
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
def predict_dataloader(self) -> DataLoader:
|
| 388 |
+
"""Get the training dataloader.
|
| 389 |
+
|
| 390 |
+
Returns
|
| 391 |
+
-------
|
| 392 |
+
DataLoader
|
| 393 |
+
The training dataloader.
|
| 394 |
+
|
| 395 |
+
"""
|
| 396 |
+
return DataLoader(
|
| 397 |
+
self.predict_set,
|
| 398 |
+
batch_size=self.batch_size,
|
| 399 |
+
num_workers=self.num_workers,
|
| 400 |
+
pin_memory=self.pin_memory,
|
| 401 |
+
shuffle=False,
|
| 402 |
+
collate_fn=collate,
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
def transfer_batch_to_device(
|
| 406 |
+
self,
|
| 407 |
+
batch: Dict,
|
| 408 |
+
device: torch.device,
|
| 409 |
+
dataloader_idx: int = 0, # noqa: ARG002
|
| 410 |
+
) -> Dict:
|
| 411 |
+
"""Transfer a batch to the given device.
|
| 412 |
+
|
| 413 |
+
Parameters
|
| 414 |
+
----------
|
| 415 |
+
batch : Dict
|
| 416 |
+
The batch to transfer.
|
| 417 |
+
device : torch.device
|
| 418 |
+
The device to transfer to.
|
| 419 |
+
|
| 420 |
+
Returns
|
| 421 |
+
-------
|
| 422 |
+
np.Any
|
| 423 |
+
The transferred batch.
|
| 424 |
+
|
| 425 |
+
"""
|
| 426 |
+
for key in batch:
|
| 427 |
+
if key not in [
|
| 428 |
+
"all_coords",
|
| 429 |
+
"all_resolved_mask",
|
| 430 |
+
"crop_to_all_atom_map",
|
| 431 |
+
"chain_symmetries",
|
| 432 |
+
"amino_acids_symmetries",
|
| 433 |
+
"ligand_symmetries",
|
| 434 |
+
"activity_name",
|
| 435 |
+
"activity_qualifier",
|
| 436 |
+
"sid",
|
| 437 |
+
"cid",
|
| 438 |
+
"normalized_protein_accession",
|
| 439 |
+
"pair_id",
|
| 440 |
+
"record",
|
| 441 |
+
"id",
|
| 442 |
+
"structure",
|
| 443 |
+
"tokenized",
|
| 444 |
+
"structure_bonds",
|
| 445 |
+
"extra_mols",
|
| 446 |
+
"data_sample_idx",
|
| 447 |
+
]:
|
| 448 |
+
batch[key] = batch[key].to(device)
|
| 449 |
+
return batch
|
model/boltzgen/task/predict/data_ligands.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import Dict, List
|
| 4 |
+
import re
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pytorch_lightning as pl
|
| 7 |
+
import torch
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
from torch.utils.data import DataLoader
|
| 10 |
+
from rdkit.Chem import Mol
|
| 11 |
+
from boltzgen.data.data import (
|
| 12 |
+
Input,
|
| 13 |
+
Structure,
|
| 14 |
+
)
|
| 15 |
+
from boltzgen.data.feature.featurizer import Featurizer
|
| 16 |
+
from boltzgen.data.pad import pad_to_max
|
| 17 |
+
from boltzgen.data.mol import (
|
| 18 |
+
load_canonicals,
|
| 19 |
+
load_molecules,
|
| 20 |
+
)
|
| 21 |
+
from boltzgen.data.template.features import load_dummy_templates
|
| 22 |
+
from boltzgen.data.tokenize.tokenizer import Tokenizer
|
| 23 |
+
from boltzgen.data.template.features import (
|
| 24 |
+
load_dummy_templates,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class DataConfig:
|
| 30 |
+
"""Data configuration."""
|
| 31 |
+
|
| 32 |
+
target_dir: str
|
| 33 |
+
msa_dir: str
|
| 34 |
+
moldir: str
|
| 35 |
+
min_len: int
|
| 36 |
+
max_len: int
|
| 37 |
+
target_ids: str
|
| 38 |
+
tokenizer: Tokenizer
|
| 39 |
+
featurizer: Featurizer
|
| 40 |
+
backbone_only: bool = False
|
| 41 |
+
atom14: bool = False
|
| 42 |
+
atom37: bool = False
|
| 43 |
+
design: bool = False
|
| 44 |
+
target_structure_condition: bool = False
|
| 45 |
+
multiplicity: int = 1
|
| 46 |
+
disulfide_prob: float = 1.0
|
| 47 |
+
disulfide_on: bool = False
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class Dataset:
|
| 52 |
+
"""Data holder."""
|
| 53 |
+
|
| 54 |
+
struct_dir: Path
|
| 55 |
+
record_dir: Path
|
| 56 |
+
target_ids: List[str]
|
| 57 |
+
min_len: int
|
| 58 |
+
max_len: int
|
| 59 |
+
tokenizer: Tokenizer
|
| 60 |
+
featurizer: Featurizer
|
| 61 |
+
multiplicity: int = 1
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def collate(data: List[Dict[str, Tensor]]) -> Dict[str, Tensor]:
|
| 65 |
+
"""Collate the data.
|
| 66 |
+
|
| 67 |
+
Parameters
|
| 68 |
+
----------
|
| 69 |
+
data : List[Dict[str, Tensor]]
|
| 70 |
+
The data to collate.
|
| 71 |
+
|
| 72 |
+
Returns
|
| 73 |
+
-------
|
| 74 |
+
Dict[str, Tensor]
|
| 75 |
+
The collated data.
|
| 76 |
+
|
| 77 |
+
"""
|
| 78 |
+
# Get the keys
|
| 79 |
+
keys = data[0].keys()
|
| 80 |
+
|
| 81 |
+
# Collate the data
|
| 82 |
+
collated = {}
|
| 83 |
+
for key in keys:
|
| 84 |
+
values = [d[key] for d in data]
|
| 85 |
+
|
| 86 |
+
if key not in [
|
| 87 |
+
"all_coords",
|
| 88 |
+
"all_resolved_mask",
|
| 89 |
+
"crop_to_all_atom_map",
|
| 90 |
+
"chain_symmetries",
|
| 91 |
+
"amino_acids_symmetries",
|
| 92 |
+
"ligand_symmetries",
|
| 93 |
+
"activity_name",
|
| 94 |
+
"activity_qualifier",
|
| 95 |
+
"sid",
|
| 96 |
+
"cid",
|
| 97 |
+
"aid",
|
| 98 |
+
"normalized_protein_accession",
|
| 99 |
+
"pair_id",
|
| 100 |
+
"record",
|
| 101 |
+
"id",
|
| 102 |
+
"tokenized",
|
| 103 |
+
"structure",
|
| 104 |
+
"structure_bonds",
|
| 105 |
+
"extra_mols",
|
| 106 |
+
"data_sample_idx",
|
| 107 |
+
]:
|
| 108 |
+
# Check if all have the same shape
|
| 109 |
+
shape = values[0].shape
|
| 110 |
+
if not all(v.shape == shape for v in values):
|
| 111 |
+
values = pad_to_max(values, 0)
|
| 112 |
+
else:
|
| 113 |
+
values = torch.stack(values, dim=0)
|
| 114 |
+
|
| 115 |
+
# Stack the values
|
| 116 |
+
collated[key] = values
|
| 117 |
+
|
| 118 |
+
return collated
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class PredictionDataset(torch.utils.data.Dataset):
|
| 122 |
+
"""Base iterable dataset."""
|
| 123 |
+
|
| 124 |
+
def __init__(
|
| 125 |
+
self,
|
| 126 |
+
dataset: Dataset,
|
| 127 |
+
canonicals: dict[str, Mol],
|
| 128 |
+
moldir: str,
|
| 129 |
+
backbone_only: bool = False,
|
| 130 |
+
atom14: bool = False,
|
| 131 |
+
atom37: bool = False,
|
| 132 |
+
design: bool = False,
|
| 133 |
+
target_structure_condition: bool = False,
|
| 134 |
+
disulfide_prob: float = 1.0,
|
| 135 |
+
disulfide_on: bool = False,
|
| 136 |
+
) -> None:
|
| 137 |
+
"""Initialize the training dataset.
|
| 138 |
+
|
| 139 |
+
Parameters
|
| 140 |
+
----------
|
| 141 |
+
datasets : List[Dataset]
|
| 142 |
+
The datasets to sample from.
|
| 143 |
+
|
| 144 |
+
"""
|
| 145 |
+
super().__init__()
|
| 146 |
+
self.dataset = dataset
|
| 147 |
+
self.moldir = moldir
|
| 148 |
+
self.canonicals = canonicals
|
| 149 |
+
self.backbone_only = backbone_only
|
| 150 |
+
self.atom14 = atom14
|
| 151 |
+
self.design = design
|
| 152 |
+
self.target_structure_condition = target_structure_condition
|
| 153 |
+
self.atom37 = atom37
|
| 154 |
+
self.disulfide_prob = disulfide_prob
|
| 155 |
+
self.disulfide_on = disulfide_on
|
| 156 |
+
|
| 157 |
+
def __getitem__(self, idx: int) -> Dict:
|
| 158 |
+
"""Get an item from the dataset.
|
| 159 |
+
|
| 160 |
+
Returns
|
| 161 |
+
-------
|
| 162 |
+
Dict[str, Tensor]
|
| 163 |
+
The sampled data features.
|
| 164 |
+
|
| 165 |
+
"""
|
| 166 |
+
# Get a sample from the dataset
|
| 167 |
+
data_sample_idx = idx // len(self.dataset.target_ids)
|
| 168 |
+
target_id = self.dataset.target_ids[idx % len(self.dataset.target_ids)]
|
| 169 |
+
|
| 170 |
+
# Get the structure
|
| 171 |
+
try:
|
| 172 |
+
target_id = target_id.lower()
|
| 173 |
+
ccd = target_id.split("_")[0]
|
| 174 |
+
pdb_id = target_id.split("_")[1].lower()
|
| 175 |
+
str_native = Structure.load(self.dataset.struct_dir / f"{pdb_id}.npz")
|
| 176 |
+
res_selection = str_native.residues["name"] == ccd.upper()
|
| 177 |
+
if res_selection.sum() > 1:
|
| 178 |
+
print(f"CCD {ccd} occurs multiple times in {pdb_id}. Using first.")
|
| 179 |
+
res_selection = np.arange(len(res_selection))[res_selection][[0]]
|
| 180 |
+
str_target = Structure.extract_residues(str_native, res_selection)
|
| 181 |
+
|
| 182 |
+
if self.dataset.min_len == self.dataset.max_len:
|
| 183 |
+
length = self.dataset.min_len
|
| 184 |
+
else:
|
| 185 |
+
length = np.random.random_integers(
|
| 186 |
+
self.dataset.min_len, self.dataset.max_len
|
| 187 |
+
)
|
| 188 |
+
str_prot = Structure.empty_protein(seq_len=length)
|
| 189 |
+
structure = Structure.concatenate(str_prot, str_target)
|
| 190 |
+
|
| 191 |
+
except Exception as e: # noqa: BLE001
|
| 192 |
+
print(f"Failed to load input for {target_id} with error {e}. Skipping.") # noqa: T201
|
| 193 |
+
return self.__getitem__(0)
|
| 194 |
+
|
| 195 |
+
# Check if the structure is valid
|
| 196 |
+
try:
|
| 197 |
+
tokenized = self.dataset.tokenizer.tokenize(structure)
|
| 198 |
+
except Exception as e: # noqa: BLE001
|
| 199 |
+
print(f"Tokenizer failed on {target_id} with error {e}. Skipping.") # noqa: T201
|
| 200 |
+
return self.__getitem__(0)
|
| 201 |
+
|
| 202 |
+
# Design mask logic
|
| 203 |
+
tokenized.tokens["design_mask"] = (
|
| 204 |
+
tokenized.tokens["asym_id"] == tokenized.tokens["asym_id"][0]
|
| 205 |
+
).astype(tokenized.tokens["asym_id"].dtype)
|
| 206 |
+
|
| 207 |
+
# Propagate design mask to obtain chain_design_mask (True whenever something is covalently bound to any residue that is in a chain that contains a design residue).
|
| 208 |
+
chain_design_mask = tokenized.tokens["design_mask"].astype(bool)
|
| 209 |
+
asym_id = tokenized.tokens["asym_id"]
|
| 210 |
+
while True:
|
| 211 |
+
design_chains = np.unique(asym_id[chain_design_mask])
|
| 212 |
+
chain_propagated = np.isin(asym_id, design_chains)
|
| 213 |
+
for i, j, _ in tokenized.bonds:
|
| 214 |
+
if any([chain_propagated[i], chain_propagated[j]]):
|
| 215 |
+
chain_propagated[i] = True
|
| 216 |
+
chain_propagated[j] = True
|
| 217 |
+
if np.equal(chain_propagated, chain_design_mask).all():
|
| 218 |
+
break
|
| 219 |
+
chain_design_mask = chain_propagated.astype(bool)
|
| 220 |
+
|
| 221 |
+
try:
|
| 222 |
+
# Try to find molecules in the dataset moldir if provided
|
| 223 |
+
# Find missing ones in global moldir and check if all found
|
| 224 |
+
molecules = {}
|
| 225 |
+
molecules.update(self.canonicals)
|
| 226 |
+
mol_names = set(tokenized.tokens["res_name"].tolist())
|
| 227 |
+
mol_names = mol_names - set(self.canonicals.keys())
|
| 228 |
+
if self.moldir is not None:
|
| 229 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 230 |
+
|
| 231 |
+
mol_names = mol_names - set(molecules.keys())
|
| 232 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 233 |
+
except Exception as e: # noqa: BLE001
|
| 234 |
+
print(f"Molecule loading failed for {target_id} with error {e}. Skipping.")
|
| 235 |
+
return self.__getitem__(0)
|
| 236 |
+
|
| 237 |
+
# Finalize input data
|
| 238 |
+
input_data = Input(
|
| 239 |
+
tokens=tokenized.tokens,
|
| 240 |
+
bonds=tokenized.bonds,
|
| 241 |
+
token_to_res=tokenized.token_to_res,
|
| 242 |
+
structure=structure,
|
| 243 |
+
msa={},
|
| 244 |
+
templates=None,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
# Compute features
|
| 248 |
+
try:
|
| 249 |
+
features = self.dataset.featurizer.process(
|
| 250 |
+
input_data,
|
| 251 |
+
molecules=molecules,
|
| 252 |
+
random=np.random.default_rng(None),
|
| 253 |
+
training=False,
|
| 254 |
+
max_seqs=1,
|
| 255 |
+
backbone_only=self.backbone_only,
|
| 256 |
+
atom14=self.atom14,
|
| 257 |
+
atom37=self.atom37,
|
| 258 |
+
design=self.design,
|
| 259 |
+
override_method="X-RAY DIFFRACTION",
|
| 260 |
+
disulfide_prob=self.disulfide_prob,
|
| 261 |
+
disulfide_on=self.disulfide_on,
|
| 262 |
+
)
|
| 263 |
+
except Exception as e: # noqa: BLE001
|
| 264 |
+
print(f"Featurizer failed on {target_id} with error {e}. Skipping.") # noqa: T201
|
| 265 |
+
return self.__getitem__(0)
|
| 266 |
+
|
| 267 |
+
# set chain_design_mask
|
| 268 |
+
features["chain_design_mask"] = torch.from_numpy(chain_design_mask)
|
| 269 |
+
|
| 270 |
+
# Compute template features
|
| 271 |
+
templates_features = load_dummy_templates(
|
| 272 |
+
tdim=1, num_tokens=len(features["res_type"])
|
| 273 |
+
)
|
| 274 |
+
features.update(templates_features)
|
| 275 |
+
|
| 276 |
+
features["idx_dataset"] = torch.tensor(1)
|
| 277 |
+
|
| 278 |
+
def sanitize_filename(name):
|
| 279 |
+
return re.sub(r"[^A-Za-z0-9._-]", "_", name)
|
| 280 |
+
|
| 281 |
+
features["id"] = sanitize_filename(target_id)
|
| 282 |
+
if self.dataset.multiplicity > 1:
|
| 283 |
+
features["data_sample_idx"] = data_sample_idx
|
| 284 |
+
return features
|
| 285 |
+
|
| 286 |
+
def __len__(self) -> int:
|
| 287 |
+
"""Get the length of the dataset.
|
| 288 |
+
|
| 289 |
+
Returns
|
| 290 |
+
-------
|
| 291 |
+
int
|
| 292 |
+
The length of the dataset.
|
| 293 |
+
|
| 294 |
+
"""
|
| 295 |
+
return len(self.dataset.target_ids) * self.dataset.multiplicity
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
class LigandBinderDataModule(pl.LightningDataModule):
|
| 299 |
+
"""DataModule for BoltzGen."""
|
| 300 |
+
|
| 301 |
+
def __init__(self, cfg: DataConfig, batch_size, num_workers, pin_memory) -> None:
|
| 302 |
+
"""Initialize the DataModule.
|
| 303 |
+
|
| 304 |
+
Parameters
|
| 305 |
+
----------
|
| 306 |
+
config : DataConfig
|
| 307 |
+
The data configuration.
|
| 308 |
+
|
| 309 |
+
"""
|
| 310 |
+
super().__init__()
|
| 311 |
+
self.cfg = cfg
|
| 312 |
+
self.batch_size = batch_size
|
| 313 |
+
self.num_workers = num_workers
|
| 314 |
+
self.pin_memory = pin_memory
|
| 315 |
+
|
| 316 |
+
with Path(cfg.target_ids).open("r") as f:
|
| 317 |
+
target_ids = [x for x in f.read().splitlines()]
|
| 318 |
+
print("split", target_ids)
|
| 319 |
+
|
| 320 |
+
dataset = Dataset(
|
| 321 |
+
struct_dir=Path(cfg.target_dir) / "structures",
|
| 322 |
+
record_dir=Path(cfg.target_dir) / "records",
|
| 323 |
+
target_ids=target_ids,
|
| 324 |
+
multiplicity=cfg.multiplicity,
|
| 325 |
+
min_len=cfg.min_len,
|
| 326 |
+
max_len=cfg.max_len,
|
| 327 |
+
tokenizer=cfg.tokenizer,
|
| 328 |
+
featurizer=cfg.featurizer,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
# Load canonical molecules
|
| 332 |
+
canonicals = load_canonicals(cfg.moldir)
|
| 333 |
+
|
| 334 |
+
self.predict_set = PredictionDataset(
|
| 335 |
+
dataset=dataset,
|
| 336 |
+
canonicals=canonicals,
|
| 337 |
+
moldir=Path(cfg.moldir),
|
| 338 |
+
backbone_only=cfg.backbone_only,
|
| 339 |
+
atom14=cfg.atom14,
|
| 340 |
+
design=cfg.design,
|
| 341 |
+
target_structure_condition=cfg.target_structure_condition,
|
| 342 |
+
disulfide_prob=cfg.disulfide_prob,
|
| 343 |
+
disulfide_on=cfg.disulfide_on,
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
def predict_dataloader(self) -> DataLoader:
|
| 347 |
+
"""Get the training dataloader.
|
| 348 |
+
|
| 349 |
+
Returns
|
| 350 |
+
-------
|
| 351 |
+
DataLoader
|
| 352 |
+
The training dataloader.
|
| 353 |
+
|
| 354 |
+
"""
|
| 355 |
+
return DataLoader(
|
| 356 |
+
self.predict_set,
|
| 357 |
+
batch_size=self.batch_size,
|
| 358 |
+
num_workers=self.num_workers,
|
| 359 |
+
pin_memory=self.pin_memory,
|
| 360 |
+
shuffle=False,
|
| 361 |
+
collate_fn=collate,
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
def transfer_batch_to_device(
|
| 365 |
+
self,
|
| 366 |
+
batch: Dict,
|
| 367 |
+
device: torch.device,
|
| 368 |
+
dataloader_idx: int, # noqa: ARG002
|
| 369 |
+
) -> Dict:
|
| 370 |
+
"""Transfer a batch to the given device.
|
| 371 |
+
|
| 372 |
+
Parameters
|
| 373 |
+
----------
|
| 374 |
+
batch : Dict
|
| 375 |
+
The batch to transfer.
|
| 376 |
+
device : torch.device
|
| 377 |
+
The device to transfer to.
|
| 378 |
+
dataloader_idx : int
|
| 379 |
+
The dataloader index.
|
| 380 |
+
|
| 381 |
+
Returns
|
| 382 |
+
-------
|
| 383 |
+
np.Any
|
| 384 |
+
The transferred batch.
|
| 385 |
+
|
| 386 |
+
"""
|
| 387 |
+
for key in batch:
|
| 388 |
+
if key not in [
|
| 389 |
+
"all_coords",
|
| 390 |
+
"all_resolved_mask",
|
| 391 |
+
"crop_to_all_atom_map",
|
| 392 |
+
"chain_symmetries",
|
| 393 |
+
"amino_acids_symmetries",
|
| 394 |
+
"ligand_symmetries",
|
| 395 |
+
"activity_name",
|
| 396 |
+
"activity_qualifier",
|
| 397 |
+
"sid",
|
| 398 |
+
"cid",
|
| 399 |
+
"normalized_protein_accession",
|
| 400 |
+
"pair_id",
|
| 401 |
+
"record",
|
| 402 |
+
"id",
|
| 403 |
+
"tokenized",
|
| 404 |
+
"structure",
|
| 405 |
+
"structure_bonds",
|
| 406 |
+
"extra_mols",
|
| 407 |
+
"data_sample_idx",
|
| 408 |
+
]:
|
| 409 |
+
batch[key] = batch[key].to(device)
|
| 410 |
+
return batch
|
model/boltzgen/task/predict/data_protein_binder.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from boltzgen.data.data import StructureInfo
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import re
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pytorch_lightning as pl
|
| 9 |
+
import torch
|
| 10 |
+
from torch import Tensor
|
| 11 |
+
from torch.utils.data import DataLoader
|
| 12 |
+
from rdkit.Chem import Mol
|
| 13 |
+
from boltzgen.data import const
|
| 14 |
+
from boltzgen.data.data import Input, Structure, Record, MSA
|
| 15 |
+
from boltzgen.data.feature.featurizer import Featurizer
|
| 16 |
+
from boltzgen.data.pad import pad_to_max
|
| 17 |
+
from boltzgen.data.mol import load_canonicals, load_molecules
|
| 18 |
+
from boltzgen.data.tokenize.tokenizer import Tokenizer
|
| 19 |
+
from boltzgen.data.template.features import (
|
| 20 |
+
load_dummy_templates,
|
| 21 |
+
)
|
| 22 |
+
from boltzgen.task.predict.loading_utils import load_record, load_structure
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class DataConfig:
|
| 27 |
+
"""Data configuration."""
|
| 28 |
+
|
| 29 |
+
target_dir: str
|
| 30 |
+
msa_dir: str
|
| 31 |
+
moldir: str
|
| 32 |
+
seq_len: int
|
| 33 |
+
target_ids: str
|
| 34 |
+
tokenizer: Tokenizer
|
| 35 |
+
featurizer: Featurizer
|
| 36 |
+
backbone_only: bool = False
|
| 37 |
+
atom14: bool = False
|
| 38 |
+
atom37: bool = False
|
| 39 |
+
design: bool = False
|
| 40 |
+
target_structure_condition: bool = False
|
| 41 |
+
msa_condition: bool = False
|
| 42 |
+
max_seq: int = 1024
|
| 43 |
+
ss_single_condition: bool = False
|
| 44 |
+
ss_double_condition: bool = False
|
| 45 |
+
ss_short_motif: bool = False
|
| 46 |
+
multiplicity: int = 1
|
| 47 |
+
inverse_fold: bool = False
|
| 48 |
+
num_targets: int = 100000
|
| 49 |
+
disulfide_prob: float = 1.0
|
| 50 |
+
disulfide_on: bool = False
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass
|
| 54 |
+
class Dataset:
|
| 55 |
+
"""Data holder."""
|
| 56 |
+
|
| 57 |
+
struct_dir: Path
|
| 58 |
+
record_dir: Path
|
| 59 |
+
target_ids: List[str]
|
| 60 |
+
seq_len: int
|
| 61 |
+
tokenizer: Tokenizer
|
| 62 |
+
featurizer: Featurizer
|
| 63 |
+
chain_ids: List[str] = None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def ss_single(feats, random):
|
| 67 |
+
feats["design_ss_mask"].zero_()
|
| 68 |
+
feats["ss_type"].zero_()
|
| 69 |
+
design_mask = feats["design_mask"].bool()
|
| 70 |
+
seq_len = design_mask.sum().item()
|
| 71 |
+
|
| 72 |
+
feats["design_ss_mask"][design_mask] = 0
|
| 73 |
+
design_indices = torch.where(design_mask)[0]
|
| 74 |
+
|
| 75 |
+
motif_length = random.integers(5, min(30, seq_len) + 1)
|
| 76 |
+
start_pos = random.choice(design_indices[: seq_len - motif_length + 1].cpu())
|
| 77 |
+
|
| 78 |
+
motif_type = random.choice([2, 3]) # helix or sheet
|
| 79 |
+
feats["design_ss_mask"][start_pos : start_pos + motif_length] = 1
|
| 80 |
+
feats["ss_type"][start_pos : start_pos + motif_length] = motif_type
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def ss_double(feats, random):
|
| 84 |
+
feats["design_ss_mask"].zero_()
|
| 85 |
+
feats["ss_type"].zero_()
|
| 86 |
+
design_mask = feats["design_mask"].bool()
|
| 87 |
+
seq_len = design_mask.sum().item()
|
| 88 |
+
|
| 89 |
+
feats["design_ss_mask"][design_mask] = 0
|
| 90 |
+
design_indices = torch.where(design_mask)[0]
|
| 91 |
+
|
| 92 |
+
motif_len1 = random.integers(5, min(30, seq_len - 10) + 1)
|
| 93 |
+
motif_len2 = random.integers(5, min(30, seq_len - motif_len1 - 5) + 1)
|
| 94 |
+
total_required = motif_len1 + 5 + motif_len2
|
| 95 |
+
start_pos = random.choice(design_indices[: seq_len - total_required + 1].cpu())
|
| 96 |
+
|
| 97 |
+
motif_type1 = random.choice([2, 3])
|
| 98 |
+
motif_type2 = random.choice([2, 3])
|
| 99 |
+
|
| 100 |
+
# first motif
|
| 101 |
+
feats["design_ss_mask"][start_pos : start_pos + motif_len1] = 1
|
| 102 |
+
feats["ss_type"][start_pos : start_pos + motif_len1] = motif_type1
|
| 103 |
+
|
| 104 |
+
# loop
|
| 105 |
+
loop_start = start_pos + motif_len1
|
| 106 |
+
feats["design_ss_mask"][loop_start : loop_start + 5] = 1
|
| 107 |
+
feats["ss_type"][loop_start : loop_start + 5] = 1 # loop
|
| 108 |
+
|
| 109 |
+
# second motif
|
| 110 |
+
second_start = loop_start + 5
|
| 111 |
+
feats["design_ss_mask"][second_start : second_start + motif_len2] = 1
|
| 112 |
+
feats["ss_type"][second_start : second_start + motif_len2] = motif_type2
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def ss_short_motif(feats, random, helix_prob=0.5):
|
| 116 |
+
feats["design_ss_mask"].zero_()
|
| 117 |
+
feats["ss_type"].zero_()
|
| 118 |
+
design_mask = feats["design_mask"].bool()
|
| 119 |
+
seq_len = design_mask.sum().item()
|
| 120 |
+
design_indices = torch.where(design_mask)[0]
|
| 121 |
+
|
| 122 |
+
start_pos = random.choice(design_indices[: seq_len - 4 + 1].cpu())
|
| 123 |
+
motif_type = 2 if random.random() < helix_prob else 3
|
| 124 |
+
|
| 125 |
+
feats["design_ss_mask"][start_pos : start_pos + 4] = 1
|
| 126 |
+
feats["ss_type"][start_pos : start_pos + 4] = motif_type
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def load_msas(record: Record, chain_ids: set[int], msa_dir: Path) -> Dict[int, MSA]:
|
| 130 |
+
"""Load the given input data.
|
| 131 |
+
|
| 132 |
+
Parameters
|
| 133 |
+
----------
|
| 134 |
+
record : Record
|
| 135 |
+
The record to load.
|
| 136 |
+
chain_ids : set[int]
|
| 137 |
+
The chain ids to load.
|
| 138 |
+
msa_dir : Path
|
| 139 |
+
The path to the MSA directory.
|
| 140 |
+
|
| 141 |
+
Returns
|
| 142 |
+
-------
|
| 143 |
+
Input
|
| 144 |
+
The loaded input.
|
| 145 |
+
|
| 146 |
+
"""
|
| 147 |
+
# Load the relevant MSAs
|
| 148 |
+
msas = {}
|
| 149 |
+
for chain in record.chains:
|
| 150 |
+
if chain.chain_id in chain_ids:
|
| 151 |
+
msa_id = chain.msa_id
|
| 152 |
+
if msa_id != -1:
|
| 153 |
+
msa = np.load(msa_dir / f"{msa_id}.npz")
|
| 154 |
+
msas[chain.chain_id] = MSA(**msa)
|
| 155 |
+
return msas
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def collate(data: List[Dict[str, Tensor]]) -> Dict[str, Tensor]:
|
| 159 |
+
"""Collate the data.
|
| 160 |
+
|
| 161 |
+
Parameters
|
| 162 |
+
----------
|
| 163 |
+
data : List[Dict[str, Tensor]]
|
| 164 |
+
The data to collate.
|
| 165 |
+
|
| 166 |
+
Returns
|
| 167 |
+
-------
|
| 168 |
+
Dict[str, Tensor]
|
| 169 |
+
The collated data.
|
| 170 |
+
|
| 171 |
+
"""
|
| 172 |
+
# Get the keys
|
| 173 |
+
keys = data[0].keys()
|
| 174 |
+
|
| 175 |
+
# Collate the data
|
| 176 |
+
collated = {}
|
| 177 |
+
for key in keys:
|
| 178 |
+
values = [d[key] for d in data]
|
| 179 |
+
|
| 180 |
+
if key not in [
|
| 181 |
+
"all_coords",
|
| 182 |
+
"all_resolved_mask",
|
| 183 |
+
"crop_to_all_atom_map",
|
| 184 |
+
"chain_symmetries",
|
| 185 |
+
"amino_acids_symmetries",
|
| 186 |
+
"ligand_symmetries",
|
| 187 |
+
"activity_name",
|
| 188 |
+
"activity_qualifier",
|
| 189 |
+
"sid",
|
| 190 |
+
"cid",
|
| 191 |
+
"aid",
|
| 192 |
+
"normalized_protein_accession",
|
| 193 |
+
"pair_id",
|
| 194 |
+
"record",
|
| 195 |
+
"id",
|
| 196 |
+
"structure_bonds",
|
| 197 |
+
"extra_mols",
|
| 198 |
+
"structure",
|
| 199 |
+
"tokenized",
|
| 200 |
+
"data_sample_idx",
|
| 201 |
+
]:
|
| 202 |
+
# Check if all have the same shape
|
| 203 |
+
shape = values[0].shape
|
| 204 |
+
if not all(v.shape == shape for v in values):
|
| 205 |
+
values = pad_to_max(values, 0)
|
| 206 |
+
else:
|
| 207 |
+
values = torch.stack(values, dim=0)
|
| 208 |
+
|
| 209 |
+
# Stack the values
|
| 210 |
+
collated[key] = values
|
| 211 |
+
|
| 212 |
+
return collated
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class PredictionDataset(torch.utils.data.Dataset):
|
| 216 |
+
"""Base iterable dataset."""
|
| 217 |
+
|
| 218 |
+
def __init__(
|
| 219 |
+
self,
|
| 220 |
+
dataset: Dataset,
|
| 221 |
+
canonicals: dict[str, Mol],
|
| 222 |
+
moldir: str,
|
| 223 |
+
backbone_only: bool = False,
|
| 224 |
+
atom14: bool = False,
|
| 225 |
+
atom37: bool = False,
|
| 226 |
+
design: bool = False,
|
| 227 |
+
target_structure_condition: bool = False,
|
| 228 |
+
msa_condition: bool = False,
|
| 229 |
+
ss_single_condition: bool = False,
|
| 230 |
+
ss_double_condition: bool = False,
|
| 231 |
+
ss_short_motif: bool = False,
|
| 232 |
+
max_seq: int = 1024,
|
| 233 |
+
msa_dir: str = "/data/rbg/shared/projects/foldeverything/rcsb/msa",
|
| 234 |
+
inverse_fold: bool = False,
|
| 235 |
+
multiplicity: int = 1,
|
| 236 |
+
disulfide_prob: float = 1.0,
|
| 237 |
+
disulfide_on: bool = False,
|
| 238 |
+
extra_features: Optional[List[str]] = None,
|
| 239 |
+
) -> None:
|
| 240 |
+
"""Initialize the training dataset.
|
| 241 |
+
|
| 242 |
+
Parameters
|
| 243 |
+
----------
|
| 244 |
+
datasets : List[Dataset]
|
| 245 |
+
The datasets to sample from.
|
| 246 |
+
|
| 247 |
+
"""
|
| 248 |
+
super().__init__()
|
| 249 |
+
self.dataset = dataset
|
| 250 |
+
self.moldir = moldir
|
| 251 |
+
self.canonicals = canonicals
|
| 252 |
+
self.backbone_only = backbone_only
|
| 253 |
+
self.atom14 = atom14
|
| 254 |
+
self.atom37 = atom37
|
| 255 |
+
self.design = design
|
| 256 |
+
self.target_structure_condition = target_structure_condition
|
| 257 |
+
self.msa_condition = msa_condition
|
| 258 |
+
self.max_seq = max_seq
|
| 259 |
+
self.msa_dir = msa_dir
|
| 260 |
+
self.inverse_fold = inverse_fold
|
| 261 |
+
self.ss_single_condition = ss_single_condition
|
| 262 |
+
self.ss_double_condition = ss_double_condition
|
| 263 |
+
self.ss_short_motif = ss_short_motif
|
| 264 |
+
self.multiplicity = multiplicity
|
| 265 |
+
self.disulfide_prob = disulfide_prob
|
| 266 |
+
self.disulfide_on = disulfide_on
|
| 267 |
+
self.extra_features = (
|
| 268 |
+
set(extra_features) if extra_features is not None else set()
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
def __getitem__(self, idx: int) -> Dict:
|
| 272 |
+
"""Get an item from the dataset.
|
| 273 |
+
|
| 274 |
+
Returns
|
| 275 |
+
-------
|
| 276 |
+
Dict[str, Tensor]
|
| 277 |
+
The sampled data features.
|
| 278 |
+
|
| 279 |
+
"""
|
| 280 |
+
# Get a sample from the dataset
|
| 281 |
+
data_sample_idx = idx // len(self.dataset.target_ids)
|
| 282 |
+
pdb_id = self.dataset.target_ids[idx % len(self.dataset.target_ids)]
|
| 283 |
+
if not self.dataset.chain_ids is None:
|
| 284 |
+
chain_id = self.dataset.chain_ids[idx % len(self.dataset.target_ids)]
|
| 285 |
+
else:
|
| 286 |
+
chain_id = None
|
| 287 |
+
|
| 288 |
+
# Load record
|
| 289 |
+
record = load_record(pdb_id, self.dataset.record_dir)
|
| 290 |
+
|
| 291 |
+
# Get the structure
|
| 292 |
+
try:
|
| 293 |
+
if self.inverse_fold:
|
| 294 |
+
structure = load_structure(record, self.dataset.struct_dir)
|
| 295 |
+
else:
|
| 296 |
+
str_native = load_structure(record, self.dataset.struct_dir)
|
| 297 |
+
|
| 298 |
+
# If chain id is specified, extract chain
|
| 299 |
+
if not chain_id is None:
|
| 300 |
+
chain_id = chain_id.lower()
|
| 301 |
+
chain = None
|
| 302 |
+
chain_names = []
|
| 303 |
+
for _chain in str_native.chains:
|
| 304 |
+
chain_names.append(_chain[0].lower())
|
| 305 |
+
if _chain[0].lower() == chain_id:
|
| 306 |
+
chain = _chain
|
| 307 |
+
if chain is None:
|
| 308 |
+
chain_names = "\n".join(chain_names)
|
| 309 |
+
msg = f"Could not find chain {chain_id}. Structure contains chains:\n{chain_names}"
|
| 310 |
+
e = ValueError(msg)
|
| 311 |
+
res_idx, res_num = chain[7], chain[8]
|
| 312 |
+
res_idxs = np.arange(res_idx, res_idx + res_num)
|
| 313 |
+
str_native = str_native.extract_residues(str_native, res_idxs)
|
| 314 |
+
|
| 315 |
+
str_prot = Structure.empty_protein(seq_len=self.dataset.seq_len)
|
| 316 |
+
structure = Structure.concatenate(str_native, str_prot)
|
| 317 |
+
except Exception as e: # noqa: BLE001
|
| 318 |
+
print(f"Failed to load input for {pdb_id} with error {e}. Skipping.") # noqa: T201
|
| 319 |
+
return self.__getitem__(0)
|
| 320 |
+
|
| 321 |
+
# Tokenize structure
|
| 322 |
+
try:
|
| 323 |
+
tokenized = self.dataset.tokenizer.tokenize(structure)
|
| 324 |
+
except Exception as e: # noqa: BLE001
|
| 325 |
+
print(f"Tokenizer failed on {pdb_id} with error {e}. Skipping.") # noqa: T201
|
| 326 |
+
return self.__getitem__(0)
|
| 327 |
+
|
| 328 |
+
if self.inverse_fold:
|
| 329 |
+
tokenized.tokens["design_mask"] = (
|
| 330 |
+
tokenized.tokens["is_standard"]
|
| 331 |
+
& (tokenized.tokens["mol_type"] == const.chain_type_ids["PROTEIN"])
|
| 332 |
+
& tokenized.tokens["resolved_mask"]
|
| 333 |
+
)
|
| 334 |
+
tokenized.tokens["structure_group"] = 1
|
| 335 |
+
else:
|
| 336 |
+
# Set the last chain to be designed
|
| 337 |
+
tokenized.tokens["design_mask"] = (
|
| 338 |
+
tokenized.tokens["asym_id"] == tokenized.tokens["asym_id"][-1]
|
| 339 |
+
).astype(tokenized.tokens["asym_id"].dtype)
|
| 340 |
+
|
| 341 |
+
if self.target_structure_condition:
|
| 342 |
+
tokenized.tokens["structure_group"][
|
| 343 |
+
~tokenized.tokens["design_mask"].astype(bool)
|
| 344 |
+
] = 1
|
| 345 |
+
|
| 346 |
+
# Propagate design mask to obtain chain_design_mask (True whenever something is covalently bound to any residue that is in a chain that contains a design residue).
|
| 347 |
+
chain_design_mask = tokenized.tokens["design_mask"].astype(bool)
|
| 348 |
+
asym_id = tokenized.tokens["asym_id"]
|
| 349 |
+
while True:
|
| 350 |
+
design_chains = np.unique(asym_id[chain_design_mask])
|
| 351 |
+
chain_propagated = np.isin(asym_id, design_chains)
|
| 352 |
+
for i, j, _ in tokenized.bonds:
|
| 353 |
+
if any([chain_propagated[i], chain_propagated[j]]):
|
| 354 |
+
chain_propagated[i] = True
|
| 355 |
+
chain_propagated[j] = True
|
| 356 |
+
if np.equal(chain_propagated, chain_design_mask).all():
|
| 357 |
+
break
|
| 358 |
+
chain_design_mask = chain_propagated.astype(bool)
|
| 359 |
+
|
| 360 |
+
# Find the record with the matching pdb_id
|
| 361 |
+
msas = {}
|
| 362 |
+
if self.msa_condition:
|
| 363 |
+
chain_ids = set(tokenized.tokens["asym_id"])
|
| 364 |
+
msas = load_msas(record=record, chain_ids=chain_ids, msa_dir=self.msa_dir)
|
| 365 |
+
|
| 366 |
+
try:
|
| 367 |
+
# Try to find molecules in the dataset moldir if provided
|
| 368 |
+
# Find missing ones in global moldir and check if all found
|
| 369 |
+
molecules = {}
|
| 370 |
+
molecules.update(self.canonicals)
|
| 371 |
+
mol_names = set(tokenized.tokens["res_name"].tolist())
|
| 372 |
+
mol_names = mol_names - set(self.canonicals.keys())
|
| 373 |
+
if self.moldir is not None:
|
| 374 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 375 |
+
|
| 376 |
+
mol_names = mol_names - set(molecules.keys())
|
| 377 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 378 |
+
except Exception as e: # noqa: BLE001
|
| 379 |
+
print(f"Molecule loading failed for {record.id} with error {e}. Skipping.")
|
| 380 |
+
return self.__getitem__(0)
|
| 381 |
+
|
| 382 |
+
# Finalize input data
|
| 383 |
+
input_data = Input(
|
| 384 |
+
tokens=tokenized.tokens,
|
| 385 |
+
bonds=tokenized.bonds,
|
| 386 |
+
token_to_res=tokenized.token_to_res,
|
| 387 |
+
structure=structure,
|
| 388 |
+
msa=msas,
|
| 389 |
+
templates=None,
|
| 390 |
+
record=record,
|
| 391 |
+
)
|
| 392 |
+
# Compute features
|
| 393 |
+
try:
|
| 394 |
+
features = self.dataset.featurizer.process(
|
| 395 |
+
input_data,
|
| 396 |
+
molecules=molecules,
|
| 397 |
+
random=np.random.default_rng(None),
|
| 398 |
+
training=False,
|
| 399 |
+
max_seqs=self.max_seq, # if self.msa_condition else 1,
|
| 400 |
+
backbone_only=self.backbone_only,
|
| 401 |
+
atom14=self.atom14,
|
| 402 |
+
atom37=self.atom37,
|
| 403 |
+
design=self.design,
|
| 404 |
+
pad_to_max_seqs=True,
|
| 405 |
+
override_method="X-RAY DIFFRACTION",
|
| 406 |
+
disulfide_prob=self.disulfide_prob,
|
| 407 |
+
disulfide_on=self.disulfide_on,
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
except Exception as e: # noqa: BLE001
|
| 411 |
+
print(f"Featurizer failed on {pdb_id} with error {e}. Skipping.") # noqa: T201
|
| 412 |
+
return self.__getitem__(0)
|
| 413 |
+
|
| 414 |
+
# set chain_design_mask
|
| 415 |
+
features["chain_design_mask"] = torch.from_numpy(chain_design_mask)
|
| 416 |
+
|
| 417 |
+
# Compute template features
|
| 418 |
+
templates_features = load_dummy_templates(
|
| 419 |
+
tdim=1, num_tokens=len(features["res_type"])
|
| 420 |
+
)
|
| 421 |
+
features.update(templates_features)
|
| 422 |
+
|
| 423 |
+
rng = np.random.default_rng(None)
|
| 424 |
+
if self.ss_single_condition:
|
| 425 |
+
ss_single(features, rng)
|
| 426 |
+
if self.ss_double_condition:
|
| 427 |
+
ss_double(features, rng)
|
| 428 |
+
if self.ss_short_motif:
|
| 429 |
+
ss_short_motif(features, rng, helix_prob=0.8)
|
| 430 |
+
|
| 431 |
+
features["idx_dataset"] = torch.tensor(1)
|
| 432 |
+
features["id"] = pdb_id
|
| 433 |
+
if "structure" in self.extra_features:
|
| 434 |
+
features["structure"] = structure
|
| 435 |
+
if "tokenized" in self.extra_features:
|
| 436 |
+
features["tokenized"] = tokenized
|
| 437 |
+
if self.multiplicity > 1:
|
| 438 |
+
features["data_sample_idx"] = data_sample_idx
|
| 439 |
+
return features
|
| 440 |
+
|
| 441 |
+
def __len__(self) -> int:
|
| 442 |
+
"""Get the length of the dataset.
|
| 443 |
+
|
| 444 |
+
Returns
|
| 445 |
+
-------
|
| 446 |
+
int
|
| 447 |
+
The length of the dataset.
|
| 448 |
+
|
| 449 |
+
"""
|
| 450 |
+
return len(self.dataset.target_ids) * self.multiplicity
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
class ProteinBinderDataModule(pl.LightningDataModule):
|
| 454 |
+
"""DataModule for BoltzGen."""
|
| 455 |
+
|
| 456 |
+
def __init__(
|
| 457 |
+
self,
|
| 458 |
+
cfg: DataConfig,
|
| 459 |
+
batch_size,
|
| 460 |
+
num_workers,
|
| 461 |
+
pin_memory,
|
| 462 |
+
extra_features: Optional[List[str]] = None,
|
| 463 |
+
) -> None:
|
| 464 |
+
"""Initialize the DataModule.
|
| 465 |
+
|
| 466 |
+
Parameters
|
| 467 |
+
----------
|
| 468 |
+
config : DataConfig
|
| 469 |
+
The data configuration.
|
| 470 |
+
|
| 471 |
+
"""
|
| 472 |
+
super().__init__()
|
| 473 |
+
self.cfg = cfg
|
| 474 |
+
self.batch_size = batch_size
|
| 475 |
+
self.num_workers = num_workers
|
| 476 |
+
self.pin_memory = pin_memory
|
| 477 |
+
|
| 478 |
+
with Path(cfg.target_ids).open("r") as f:
|
| 479 |
+
regex = re.compile(r"([^_]+)(?:_(.*))?")
|
| 480 |
+
target_ids = [
|
| 481 |
+
regex.match(x.lower()).groups() for x in f.read().splitlines()
|
| 482 |
+
]
|
| 483 |
+
chain_ids = [tup[1] for tup in target_ids]
|
| 484 |
+
target_ids = [tup[0] for tup in target_ids]
|
| 485 |
+
if cfg.num_targets is not None:
|
| 486 |
+
target_ids = target_ids[: cfg.num_targets]
|
| 487 |
+
print("split", target_ids)
|
| 488 |
+
|
| 489 |
+
dataset = Dataset(
|
| 490 |
+
struct_dir=Path(cfg.target_dir) / "structures",
|
| 491 |
+
record_dir=Path(cfg.target_dir) / "records",
|
| 492 |
+
target_ids=target_ids,
|
| 493 |
+
seq_len=cfg.seq_len,
|
| 494 |
+
tokenizer=cfg.tokenizer,
|
| 495 |
+
featurizer=cfg.featurizer,
|
| 496 |
+
chain_ids=chain_ids,
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
# Load canonical molecules
|
| 500 |
+
canonicals = load_canonicals(cfg.moldir)
|
| 501 |
+
|
| 502 |
+
self.predict_set = PredictionDataset(
|
| 503 |
+
dataset=dataset,
|
| 504 |
+
canonicals=canonicals,
|
| 505 |
+
moldir=Path(cfg.moldir),
|
| 506 |
+
backbone_only=cfg.backbone_only,
|
| 507 |
+
atom14=cfg.atom14,
|
| 508 |
+
atom37=cfg.atom37,
|
| 509 |
+
design=cfg.design,
|
| 510 |
+
target_structure_condition=cfg.target_structure_condition,
|
| 511 |
+
msa_condition=cfg.msa_condition,
|
| 512 |
+
ss_single_condition=cfg.ss_single_condition,
|
| 513 |
+
ss_double_condition=cfg.ss_double_condition,
|
| 514 |
+
ss_short_motif=cfg.ss_short_motif,
|
| 515 |
+
max_seq=cfg.seq_len,
|
| 516 |
+
msa_dir=Path(cfg.msa_dir),
|
| 517 |
+
multiplicity=cfg.multiplicity,
|
| 518 |
+
inverse_fold=cfg.inverse_fold,
|
| 519 |
+
disulfide_prob=cfg.disulfide_prob,
|
| 520 |
+
disulfide_on=cfg.disulfide_on,
|
| 521 |
+
extra_features=extra_features,
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
def predict_dataloader(self) -> DataLoader:
|
| 525 |
+
"""Get the training dataloader.
|
| 526 |
+
|
| 527 |
+
Returns
|
| 528 |
+
-------
|
| 529 |
+
DataLoader
|
| 530 |
+
The training dataloader.
|
| 531 |
+
|
| 532 |
+
"""
|
| 533 |
+
return DataLoader(
|
| 534 |
+
self.predict_set,
|
| 535 |
+
batch_size=self.batch_size,
|
| 536 |
+
num_workers=self.num_workers,
|
| 537 |
+
pin_memory=self.pin_memory,
|
| 538 |
+
shuffle=False,
|
| 539 |
+
collate_fn=collate,
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
def transfer_batch_to_device(
|
| 543 |
+
self,
|
| 544 |
+
batch: Dict,
|
| 545 |
+
device: torch.device,
|
| 546 |
+
dataloader_idx: int, # noqa: ARG002
|
| 547 |
+
) -> Dict:
|
| 548 |
+
"""Transfer a batch to the given device.
|
| 549 |
+
|
| 550 |
+
Parameters
|
| 551 |
+
----------
|
| 552 |
+
batch : Dict
|
| 553 |
+
The batch to transfer.
|
| 554 |
+
device : torch.device
|
| 555 |
+
The device to transfer to.
|
| 556 |
+
dataloader_idx : int
|
| 557 |
+
The dataloader index.
|
| 558 |
+
|
| 559 |
+
Returns
|
| 560 |
+
-------
|
| 561 |
+
np.Any
|
| 562 |
+
The transferred batch.
|
| 563 |
+
|
| 564 |
+
"""
|
| 565 |
+
for key in batch:
|
| 566 |
+
if key not in [
|
| 567 |
+
"all_coords",
|
| 568 |
+
"all_resolved_mask",
|
| 569 |
+
"crop_to_all_atom_map",
|
| 570 |
+
"chain_symmetries",
|
| 571 |
+
"amino_acids_symmetries",
|
| 572 |
+
"ligand_symmetries",
|
| 573 |
+
"activity_name",
|
| 574 |
+
"activity_qualifier",
|
| 575 |
+
"sid",
|
| 576 |
+
"cid",
|
| 577 |
+
"normalized_protein_accession",
|
| 578 |
+
"pair_id",
|
| 579 |
+
"record",
|
| 580 |
+
"id",
|
| 581 |
+
"structure",
|
| 582 |
+
"tokenized",
|
| 583 |
+
"structure_bonds",
|
| 584 |
+
"extra_mols",
|
| 585 |
+
"data_sample_idx",
|
| 586 |
+
]:
|
| 587 |
+
batch[key] = batch[key].to(device)
|
| 588 |
+
return batch
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
if __name__ == "__main__":
|
| 592 |
+
# debugging code
|
| 593 |
+
from omegaconf import OmegaConf
|
| 594 |
+
from hydra.utils import instantiate
|
| 595 |
+
import tqdm
|
| 596 |
+
|
| 597 |
+
cfg = OmegaConf.load("configs/predict/prot.yaml")
|
| 598 |
+
# cfg = OmegaConf.load("configs/predict/lig.yaml")
|
| 599 |
+
# datamodule = instantiate(cfg.data)
|
| 600 |
+
|
| 601 |
+
cfg.data.cfg.target_ids = "/data/scratch/faltings/data/bgen/hard_test_set_ids.txt"
|
| 602 |
+
datamodule = instantiate(cfg.data)
|
| 603 |
+
for i in tqdm.tqdm(range(len(datamodule.predict_set))):
|
| 604 |
+
try:
|
| 605 |
+
entry = datamodule.predict_set[i]
|
| 606 |
+
except Exception as e:
|
| 607 |
+
print(e)
|
| 608 |
+
continue
|
model/boltzgen/task/predict/loading_utils.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from boltzgen.data.data import Record, Structure
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def load_record(record_id: str, record_dir: Path) -> Record:
|
| 7 |
+
"""Load the given record.
|
| 8 |
+
|
| 9 |
+
Parameters
|
| 10 |
+
----------
|
| 11 |
+
record_id : str
|
| 12 |
+
The record id to load.
|
| 13 |
+
record_dir : Path
|
| 14 |
+
The path to the record directory.
|
| 15 |
+
|
| 16 |
+
Returns
|
| 17 |
+
-------
|
| 18 |
+
Record
|
| 19 |
+
The loaded record.
|
| 20 |
+
"""
|
| 21 |
+
return Record.load(record_dir / f"{record_id}.json")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_structure(record: Record, struct_dir: Path) -> Structure:
|
| 25 |
+
"""Load the given input data.
|
| 26 |
+
|
| 27 |
+
Parameters
|
| 28 |
+
----------
|
| 29 |
+
record : str
|
| 30 |
+
The record to load.
|
| 31 |
+
target_dir : Path
|
| 32 |
+
The path to the data directory.
|
| 33 |
+
|
| 34 |
+
Returns
|
| 35 |
+
-------
|
| 36 |
+
Input
|
| 37 |
+
The loaded input.
|
| 38 |
+
|
| 39 |
+
"""
|
| 40 |
+
if (struct_dir / f"{record.id}.npz").exists():
|
| 41 |
+
structure_path = struct_dir / f"{record.id}.npz"
|
| 42 |
+
else:
|
| 43 |
+
structure_path = struct_dir / f"{record.id}" / f"{record.id}_model_0.npz"
|
| 44 |
+
return Structure.load(structure_path)
|
| 45 |
+
|
model/boltzgen/task/predict/predict.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from boltzgen.utils.quiet import quiet_startup
|
| 2 |
+
|
| 3 |
+
quiet_startup()
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
# Disable Triton auto-tuning during inference
|
| 8 |
+
os.environ.setdefault("CUEQ_DEFAULT_CONFIG", "1")
|
| 9 |
+
os.environ.setdefault("CUEQ_DISABLE_AOT_TUNING", "1")
|
| 10 |
+
|
| 11 |
+
from typing import List, Optional, Union
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
from omegaconf import OmegaConf, listconfig
|
| 15 |
+
from pytorch_lightning import LightningModule, Trainer
|
| 16 |
+
|
| 17 |
+
from pytorch_lightning.strategies import DDPStrategy
|
| 18 |
+
|
| 19 |
+
from boltzgen.task.predict.data_from_generated import FromGeneratedDataModule
|
| 20 |
+
from boltzgen.task.predict.writer import (
|
| 21 |
+
DesignWriter,
|
| 22 |
+
FoldingWriter,
|
| 23 |
+
)
|
| 24 |
+
from boltzgen.task.task import Task
|
| 25 |
+
from boltzgen.utils.pipeline_progress_bar import PipelineProgressBar
|
| 26 |
+
from boltzgen.model.models.boltz import Boltz
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class Predict(Task):
|
| 30 |
+
"""A task to run model inference."""
|
| 31 |
+
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
data: Union[FromGeneratedDataModule],
|
| 35 |
+
writer: Union[DesignWriter, FoldingWriter],
|
| 36 |
+
checkpoint: str,
|
| 37 |
+
output: str,
|
| 38 |
+
name: str,
|
| 39 |
+
recycling_steps: int,
|
| 40 |
+
sampling_steps: int,
|
| 41 |
+
diffusion_samples: int = 1,
|
| 42 |
+
keys_dict_out: Optional[List] = None,
|
| 43 |
+
keys_dict_batch: Optional[List] = None,
|
| 44 |
+
slurm: bool = False,
|
| 45 |
+
matmul_precision: Optional[str] = None,
|
| 46 |
+
trainer: Optional[dict] = None,
|
| 47 |
+
override: Optional[dict] = None,
|
| 48 |
+
debug: bool = False,
|
| 49 |
+
use_ema: bool = False,
|
| 50 |
+
write_manifest: bool = False,
|
| 51 |
+
compile_pairformer: bool = False,
|
| 52 |
+
compile_structure: bool = False,
|
| 53 |
+
checkpoint_diffusion_conditioning: bool = False,
|
| 54 |
+
) -> None:
|
| 55 |
+
"""Initialize the task.
|
| 56 |
+
|
| 57 |
+
Parameters
|
| 58 |
+
----------
|
| 59 |
+
checkpoint : str
|
| 60 |
+
The path to the model checkpoint.
|
| 61 |
+
output : str
|
| 62 |
+
The path to save the inference results.
|
| 63 |
+
slurm : bool, optional
|
| 64 |
+
Whether to run on SLURM, by default False
|
| 65 |
+
matmul_precision : Optional[str], optional
|
| 66 |
+
The matmul precision, by default None
|
| 67 |
+
trainer : Optional[dict], optional
|
| 68 |
+
The configuration for the trainer, by default None
|
| 69 |
+
override : Optional[dict], optional
|
| 70 |
+
The override configuration for the model, by default None
|
| 71 |
+
|
| 72 |
+
"""
|
| 73 |
+
self.data = data
|
| 74 |
+
self.checkpoint = checkpoint
|
| 75 |
+
self.output = output
|
| 76 |
+
self.slurm = slurm
|
| 77 |
+
self.matmul_precision = matmul_precision
|
| 78 |
+
self.trainer = trainer
|
| 79 |
+
self.override = override if override is not None else {}
|
| 80 |
+
self.predict_args = {
|
| 81 |
+
"recycling_steps": recycling_steps,
|
| 82 |
+
"sampling_steps": sampling_steps,
|
| 83 |
+
"diffusion_samples": diffusion_samples,
|
| 84 |
+
}
|
| 85 |
+
if keys_dict_batch is not None:
|
| 86 |
+
self.predict_args["keys_dict_batch"] = keys_dict_batch
|
| 87 |
+
if keys_dict_out is not None:
|
| 88 |
+
self.predict_args["keys_dict_out"] = keys_dict_out
|
| 89 |
+
self.debug = debug
|
| 90 |
+
self.use_ema = use_ema
|
| 91 |
+
self.write_manifest = write_manifest
|
| 92 |
+
self.writer = writer
|
| 93 |
+
self.compile_pairformer = compile_pairformer
|
| 94 |
+
self.compile_structure = compile_structure
|
| 95 |
+
self.checkpoint_diffusion_conditioning = checkpoint_diffusion_conditioning
|
| 96 |
+
|
| 97 |
+
def run(self, config: OmegaConf = None, run_prediction=True) -> None: # noqa: ARG002
|
| 98 |
+
# Silence warnings and pytorch lightning tips
|
| 99 |
+
quiet_startup()
|
| 100 |
+
|
| 101 |
+
# Exit quickly if no predictions are needed
|
| 102 |
+
if len(self.data.predict_set) == 0:
|
| 103 |
+
print("No predictions required")
|
| 104 |
+
return
|
| 105 |
+
|
| 106 |
+
# Set no grad
|
| 107 |
+
torch.set_grad_enabled(False)
|
| 108 |
+
|
| 109 |
+
# Experiment with this during training (high or medium)
|
| 110 |
+
if self.matmul_precision is not None:
|
| 111 |
+
torch.set_float32_matmul_precision(self.matmul_precision)
|
| 112 |
+
|
| 113 |
+
# Create trainer dict
|
| 114 |
+
if self.trainer is None:
|
| 115 |
+
self.trainer = {}
|
| 116 |
+
|
| 117 |
+
# Flip some arguments in debug mode
|
| 118 |
+
devices = self.trainer.get("devices", 1)
|
| 119 |
+
|
| 120 |
+
if self.debug:
|
| 121 |
+
if isinstance(devices, int):
|
| 122 |
+
devices = 1
|
| 123 |
+
elif isinstance(devices, (list, listconfig.ListConfig)):
|
| 124 |
+
devices = [devices[0]]
|
| 125 |
+
self.trainer["devices"] = devices
|
| 126 |
+
self.data.num_workers = 0
|
| 127 |
+
|
| 128 |
+
# slurm
|
| 129 |
+
if self.slurm:
|
| 130 |
+
self.trainer["devices"] = int(
|
| 131 |
+
os.environ.get("SLURM_NTASKS_PER_NODE", "auto")
|
| 132 |
+
)
|
| 133 |
+
self.trainer["num_nodes"] = int(os.environ.get("SLURM_NNODES", 1))
|
| 134 |
+
|
| 135 |
+
# Load model
|
| 136 |
+
self.model_module: LightningModule = Boltz.load_from_checkpoint(
|
| 137 |
+
self.checkpoint,
|
| 138 |
+
strict=True,
|
| 139 |
+
use_ema=self.use_ema,
|
| 140 |
+
checkpoint_diffusion_conditioning=self.checkpoint_diffusion_conditioning,
|
| 141 |
+
map_location="cpu",
|
| 142 |
+
weights_only=False,
|
| 143 |
+
predict_args=self.predict_args,
|
| 144 |
+
**self.override,
|
| 145 |
+
)
|
| 146 |
+
self.model_module.eval()
|
| 147 |
+
|
| 148 |
+
if self.compile_pairformer:
|
| 149 |
+
self.model_module.is_pairformer_compiled = True
|
| 150 |
+
self.model_module.pairformer_module = torch.compile(
|
| 151 |
+
self.model_module.pairformer_module, dynamic=True, fullgraph=False
|
| 152 |
+
)
|
| 153 |
+
if self.compile_structure:
|
| 154 |
+
self.model_module.structure_module.score_model.is_token_transformer_compiled = True
|
| 155 |
+
self.model_module.structure_module.score_model.token_transformer = (
|
| 156 |
+
torch.compile(
|
| 157 |
+
self.model_module.structure_module.score_model.token_transformer,
|
| 158 |
+
dynamic=True,
|
| 159 |
+
fullgraph=False,
|
| 160 |
+
)
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# Set up trainer
|
| 164 |
+
strategy = "auto"
|
| 165 |
+
num_devices = (
|
| 166 |
+
len(devices)
|
| 167 |
+
if isinstance(devices, (list, listconfig.ListConfig))
|
| 168 |
+
else devices
|
| 169 |
+
)
|
| 170 |
+
if num_devices > 1:
|
| 171 |
+
strategy = DDPStrategy()
|
| 172 |
+
if num_devices > len(self.data.predict_set):
|
| 173 |
+
devices = max(1, len(self.data.predict_set))
|
| 174 |
+
msg = f"Fewer designs than devices. Setting devices to {devices}."
|
| 175 |
+
print(msg)
|
| 176 |
+
self.trainer["devices"] = devices
|
| 177 |
+
|
| 178 |
+
self.lightning_trainer = Trainer(
|
| 179 |
+
default_root_dir=self.output,
|
| 180 |
+
strategy=strategy,
|
| 181 |
+
callbacks=[self.writer]
|
| 182 |
+
+ (
|
| 183 |
+
[PipelineProgressBar()]
|
| 184 |
+
if os.environ.get("BOLTZGEN_PIPELINE_STEP")
|
| 185 |
+
else []
|
| 186 |
+
),
|
| 187 |
+
**self.trainer,
|
| 188 |
+
)
|
| 189 |
+
if run_prediction:
|
| 190 |
+
# Run training
|
| 191 |
+
self.lightning_trainer.predict(
|
| 192 |
+
self.model_module, datamodule=self.data, return_predictions=False
|
| 193 |
+
)
|
| 194 |
+
del self.model_module
|
model/boltzgen/task/predict/writer.py
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pickle
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import Dict, List
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
from pytorch_lightning import LightningModule, Trainer
|
| 8 |
+
from pytorch_lightning.callbacks import BasePredictionWriter
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
|
| 12 |
+
from boltzgen.data import const
|
| 13 |
+
from boltzgen.data.data import (
|
| 14 |
+
Structure,
|
| 15 |
+
convert_ccd,
|
| 16 |
+
)
|
| 17 |
+
from boltzgen.data.feature.featurizer import (
|
| 18 |
+
res_from_atom14,
|
| 19 |
+
res_from_atom37,
|
| 20 |
+
res_all_gly,
|
| 21 |
+
)
|
| 22 |
+
from boltzgen.data.write.mmcif import to_mmcif
|
| 23 |
+
from boltzgen.data.write.pdb import to_pdb
|
| 24 |
+
from boltzgen.model.loss.diffusion import weighted_rigid_align
|
| 25 |
+
from boltzgen.model.modules.masker import BoltzMasker
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class FoldingWriter(BasePredictionWriter):
|
| 29 |
+
"""Custom writer for predictions."""
|
| 30 |
+
|
| 31 |
+
def __init__(self, design_dir: str, designfolding: bool = False) -> None:
|
| 32 |
+
super().__init__(write_interval="batch")
|
| 33 |
+
self.designfolding = designfolding
|
| 34 |
+
if design_dir is not None:
|
| 35 |
+
self.init_outdir(design_dir)
|
| 36 |
+
|
| 37 |
+
def init_outdir(self, design_dir):
|
| 38 |
+
self.outdir = Path(design_dir) / (
|
| 39 |
+
const.folding_design_dirname
|
| 40 |
+
if self.designfolding
|
| 41 |
+
else const.folding_dirname
|
| 42 |
+
)
|
| 43 |
+
self.refold_cif_dir = Path(design_dir) / (
|
| 44 |
+
const.refold_design_cif_dirname
|
| 45 |
+
if self.designfolding
|
| 46 |
+
else const.refold_cif_dirname
|
| 47 |
+
)
|
| 48 |
+
self.refold_cif_dir.mkdir(parents=True, exist_ok=True)
|
| 49 |
+
self.outdir.mkdir(exist_ok=True, parents=True)
|
| 50 |
+
self.failed = 0
|
| 51 |
+
|
| 52 |
+
def write_on_batch_end( # noqa: PLR0915
|
| 53 |
+
self,
|
| 54 |
+
trainer: Trainer = None, # noqa: ARG002
|
| 55 |
+
pl_module: LightningModule = None, # noqa: ARG002
|
| 56 |
+
prediction: Dict[str, Tensor] = None,
|
| 57 |
+
batch_indices: List[int] = None, # noqa: ARG002
|
| 58 |
+
batch: Dict[str, Tensor] = None,
|
| 59 |
+
batch_idx: int = None, # noqa: ARG002
|
| 60 |
+
dataloader_idx: int = 0, # noqa: ARG002
|
| 61 |
+
sample_id: str = None,
|
| 62 |
+
) -> None:
|
| 63 |
+
"""Write the predictions to disk."""
|
| 64 |
+
pred_dict = {}
|
| 65 |
+
for key, value in prediction.items():
|
| 66 |
+
# check object is tensor
|
| 67 |
+
if key in const.eval_keys:
|
| 68 |
+
pred_dict[key] = value.cpu().numpy()
|
| 69 |
+
np.savez_compressed(self.outdir / f"{batch['id'][0]}.npz", **pred_dict)
|
| 70 |
+
|
| 71 |
+
# Get best sample
|
| 72 |
+
confidence = 0.8 * pred_dict["iptm"] + 0.2 * pred_dict["ptm"]
|
| 73 |
+
best_idx = np.argmax(confidence)
|
| 74 |
+
best_sample_coords = pred_dict["coords"][best_idx]
|
| 75 |
+
|
| 76 |
+
prediction_out = {}
|
| 77 |
+
for k in prediction:
|
| 78 |
+
if k == "coords":
|
| 79 |
+
prediction_out[k] = torch.from_numpy(best_sample_coords)
|
| 80 |
+
else:
|
| 81 |
+
prediction_out[k] = prediction[k][0]
|
| 82 |
+
|
| 83 |
+
# Write structure
|
| 84 |
+
structure, _, _ = Structure.from_feat(prediction_out)
|
| 85 |
+
plddt_atom = (
|
| 86 |
+
prediction_out["atom_to_token"].float() @ prediction_out["plddt"].float()
|
| 87 |
+
)
|
| 88 |
+
structure.atoms["bfactor"] = (
|
| 89 |
+
plddt_atom[prediction_out["atom_pad_mask"].bool()].float().cpu().numpy()
|
| 90 |
+
)
|
| 91 |
+
cif_text = to_mmcif(structure)
|
| 92 |
+
open(self.refold_cif_dir / f"{batch['id'][0]}.cif", "w").write(cif_text)
|
| 93 |
+
|
| 94 |
+
# Failed prediction handling
|
| 95 |
+
if isinstance(prediction["exception"], bool):
|
| 96 |
+
if prediction["exception"]:
|
| 97 |
+
self.failed += 1
|
| 98 |
+
elif isinstance(prediction["exception"], list):
|
| 99 |
+
if prediction["exception"][0]:
|
| 100 |
+
self.failed += 1
|
| 101 |
+
|
| 102 |
+
def on_predict_epoch_end(
|
| 103 |
+
self,
|
| 104 |
+
trainer: Trainer, # noqa: ARG002
|
| 105 |
+
pl_module: LightningModule, # noqa: ARG002
|
| 106 |
+
) -> None:
|
| 107 |
+
print(f"Number of failed structure predictions: {self.failed}") # noqa: T201
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class AffinityWriter(BasePredictionWriter):
|
| 111 |
+
"""Custom writer for predictions."""
|
| 112 |
+
|
| 113 |
+
def __init__(
|
| 114 |
+
self,
|
| 115 |
+
design_dir: str,
|
| 116 |
+
) -> None:
|
| 117 |
+
super().__init__(write_interval="batch")
|
| 118 |
+
if design_dir is not None:
|
| 119 |
+
self.init_outdir(design_dir)
|
| 120 |
+
|
| 121 |
+
def init_outdir(self, design_dir):
|
| 122 |
+
self.outdir = Path(design_dir) / const.affinity_dirname
|
| 123 |
+
self.outdir.mkdir(exist_ok=True, parents=True)
|
| 124 |
+
self.failed = 0
|
| 125 |
+
|
| 126 |
+
def write_on_batch_end( # noqa: PLR0915
|
| 127 |
+
self,
|
| 128 |
+
trainer: Trainer = None, # noqa: ARG002
|
| 129 |
+
pl_module: LightningModule = None, # noqa: ARG002
|
| 130 |
+
prediction: Dict[str, Tensor] = None,
|
| 131 |
+
batch_indices: List[int] = None, # noqa: ARG002
|
| 132 |
+
batch: Dict[str, Tensor] = None,
|
| 133 |
+
batch_idx: int = None, # noqa: ARG002
|
| 134 |
+
dataloader_idx: int = 0, # noqa: ARG002
|
| 135 |
+
sample_id: str = None,
|
| 136 |
+
) -> None:
|
| 137 |
+
"""Write the predictions to disk."""
|
| 138 |
+
pred_dict = {}
|
| 139 |
+
for key, value in prediction.items():
|
| 140 |
+
# check object is tensor
|
| 141 |
+
if key in const.eval_keys:
|
| 142 |
+
pred_dict[key] = value.cpu().numpy()
|
| 143 |
+
np.savez_compressed(self.outdir / f"{batch['id'][0]}.npz", **pred_dict)
|
| 144 |
+
|
| 145 |
+
if isinstance(prediction["exception"], bool):
|
| 146 |
+
if prediction["exception"]:
|
| 147 |
+
self.failed += 1
|
| 148 |
+
elif isinstance(prediction["exception"], list):
|
| 149 |
+
if prediction["exception"][0]:
|
| 150 |
+
self.failed += 1
|
| 151 |
+
|
| 152 |
+
def on_predict_epoch_end(
|
| 153 |
+
self,
|
| 154 |
+
trainer: Trainer, # noqa: ARG002
|
| 155 |
+
pl_module: LightningModule, # noqa: ARG002
|
| 156 |
+
) -> None:
|
| 157 |
+
print(f"Number of failed affinity predictions: {self.failed}") # noqa: T201
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class DesignWriter(BasePredictionWriter):
|
| 161 |
+
"""Custom writer for predictions."""
|
| 162 |
+
|
| 163 |
+
def __init__(
|
| 164 |
+
self,
|
| 165 |
+
output_dir: str,
|
| 166 |
+
res_atoms_only: bool,
|
| 167 |
+
save_traj: bool = False,
|
| 168 |
+
save_x0_traj: bool = False,
|
| 169 |
+
atom14: bool = True,
|
| 170 |
+
atom37: bool = False,
|
| 171 |
+
backbone_only: bool = False,
|
| 172 |
+
inverse_fold: bool = False,
|
| 173 |
+
file_suffix: str = "",
|
| 174 |
+
write_native: bool = True,
|
| 175 |
+
design: bool = True,
|
| 176 |
+
) -> None:
|
| 177 |
+
"""Initialize the writer.
|
| 178 |
+
|
| 179 |
+
Parameters
|
| 180 |
+
----------
|
| 181 |
+
output_dir : str
|
| 182 |
+
The directory to save the predictions.
|
| 183 |
+
|
| 184 |
+
"""
|
| 185 |
+
super().__init__(write_interval="batch")
|
| 186 |
+
self.mol_dir = Path(output_dir) / const.molecules_dirname
|
| 187 |
+
self.mol_dir.mkdir(parents=True, exist_ok=True)
|
| 188 |
+
self.save_traj = save_traj
|
| 189 |
+
self.save_x0_traj = save_x0_traj
|
| 190 |
+
self.res_atoms_only = res_atoms_only
|
| 191 |
+
self.file_suffix = file_suffix
|
| 192 |
+
self.failed = 0
|
| 193 |
+
self.write_native = write_native
|
| 194 |
+
self.design = design
|
| 195 |
+
|
| 196 |
+
# Create the output directories
|
| 197 |
+
self.atom14 = atom14
|
| 198 |
+
self.atom37 = atom37
|
| 199 |
+
self.inverse_fold = inverse_fold
|
| 200 |
+
self.backbone_only = backbone_only
|
| 201 |
+
self.used_stems = set()
|
| 202 |
+
self.init_outdir(output_dir)
|
| 203 |
+
|
| 204 |
+
def init_outdir(self, outdir):
|
| 205 |
+
self.outdir = Path(outdir)
|
| 206 |
+
self.outdir.mkdir(parents=True, exist_ok=True)
|
| 207 |
+
|
| 208 |
+
def write_on_batch_end( # noqa: PLR0915
|
| 209 |
+
self,
|
| 210 |
+
trainer: Trainer = None, # noqa: ARG002
|
| 211 |
+
pl_module: LightningModule = None, # noqa: ARG002
|
| 212 |
+
prediction: Dict[str, Tensor] = None,
|
| 213 |
+
batch_indices: List[int] = None, # noqa: ARG002
|
| 214 |
+
batch: Dict[str, Tensor] = None,
|
| 215 |
+
batch_idx: int = None, # noqa: ARG002
|
| 216 |
+
dataloader_idx: int = 0, # noqa: ARG002
|
| 217 |
+
sample_id: str = None,
|
| 218 |
+
) -> None:
|
| 219 |
+
if prediction["exception"]:
|
| 220 |
+
self.failed += 1
|
| 221 |
+
return
|
| 222 |
+
n_samples, _, _ = prediction["coords"].shape
|
| 223 |
+
|
| 224 |
+
# TODO: remove this which is only here for temporary backward compatibility
|
| 225 |
+
masker = BoltzMasker(mask=True, mask_backbone=False)
|
| 226 |
+
feat_masked = masker(batch)
|
| 227 |
+
prediction["ref_element"] = feat_masked["ref_element"]
|
| 228 |
+
prediction["ref_atom_name_chars"] = feat_masked["ref_atom_name_chars"]
|
| 229 |
+
"""Write the predictions to disk."""
|
| 230 |
+
# Check for extra molecules
|
| 231 |
+
if batch["extra_mols"] is not None:
|
| 232 |
+
extra_mols = batch["extra_mols"][0]
|
| 233 |
+
for k, v in extra_mols.items():
|
| 234 |
+
with open(self.mol_dir / f"{k}.pkl", "wb") as f:
|
| 235 |
+
pickle.dump(v, f)
|
| 236 |
+
|
| 237 |
+
# write samples to disk
|
| 238 |
+
for n in range(n_samples):
|
| 239 |
+
# get structure for all generated coords
|
| 240 |
+
sample, native = {}, {}
|
| 241 |
+
|
| 242 |
+
for k in set(prediction.keys()) & set(batch.keys()):
|
| 243 |
+
if k == "coords":
|
| 244 |
+
native[k] = batch[k][0][0].unsqueeze(0)
|
| 245 |
+
sample[k] = prediction[k][n]
|
| 246 |
+
|
| 247 |
+
if k in const.token_features:
|
| 248 |
+
sample[k] = prediction[k][0]
|
| 249 |
+
native[k] = batch[k][0]
|
| 250 |
+
elif k in const.atom_features:
|
| 251 |
+
if k == "coords":
|
| 252 |
+
native[k] = batch[k][0][0].unsqueeze(0)
|
| 253 |
+
sample[k] = prediction[k][n]
|
| 254 |
+
else:
|
| 255 |
+
native[k] = batch[k][0]
|
| 256 |
+
sample[k] = prediction[k][0]
|
| 257 |
+
elif k == "exception":
|
| 258 |
+
sample[k] = prediction[k]
|
| 259 |
+
native[k] = batch[k]
|
| 260 |
+
else:
|
| 261 |
+
native[k] = batch[k][0]
|
| 262 |
+
sample[k] = prediction[k][0]
|
| 263 |
+
native[k] = batch[k][0]
|
| 264 |
+
|
| 265 |
+
if self.atom14:
|
| 266 |
+
sample = res_from_atom14(sample)
|
| 267 |
+
elif self.atom37:
|
| 268 |
+
sample = res_from_atom37(sample)
|
| 269 |
+
elif self.backbone_only:
|
| 270 |
+
sample = res_all_gly(sample)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
design_mask = batch["design_mask"][0].bool()
|
| 274 |
+
assert design_mask.sum() == sample["design_mask"].sum()
|
| 275 |
+
|
| 276 |
+
if self.inverse_fold:
|
| 277 |
+
token_ids = torch.argmax(sample["res_type"], dim=-1)
|
| 278 |
+
tokens = [const.tokens[i] for i in token_ids]
|
| 279 |
+
ccds = [convert_ccd(token) for token in tokens]
|
| 280 |
+
|
| 281 |
+
ccds = torch.tensor(ccds).to(sample["res_type"])
|
| 282 |
+
sample["ccd"][design_mask] = ccds[design_mask]
|
| 283 |
+
|
| 284 |
+
try:
|
| 285 |
+
structure, _, _ = Structure.from_feat(sample)
|
| 286 |
+
str_native, _, _ = Structure.from_feat(native)
|
| 287 |
+
|
| 288 |
+
# write structure to cif
|
| 289 |
+
if sample_id is not None:
|
| 290 |
+
file_name = f"{sample_id}_{n}{self.file_suffix}"
|
| 291 |
+
else:
|
| 292 |
+
stem = str(batch["id"][0])
|
| 293 |
+
multiplicity = getattr(trainer.datamodule.cfg, "multiplicity", 1)
|
| 294 |
+
total_files = multiplicity * n_samples
|
| 295 |
+
sample_idx = (
|
| 296 |
+
int(batch["data_sample_idx"][0])
|
| 297 |
+
if "data_sample_idx" in batch
|
| 298 |
+
else 0
|
| 299 |
+
)
|
| 300 |
+
global_idx = sample_idx * n_samples + n
|
| 301 |
+
|
| 302 |
+
if total_files > 1:
|
| 303 |
+
num_digits = len(str(total_files - 1))
|
| 304 |
+
file_name = (
|
| 305 |
+
f"{stem}_{global_idx:0{num_digits}d}{self.file_suffix}"
|
| 306 |
+
)
|
| 307 |
+
else:
|
| 308 |
+
file_name = f"{stem}{self.file_suffix}"
|
| 309 |
+
|
| 310 |
+
native_path = f"{self.outdir}/{file_name}_native.cif"
|
| 311 |
+
gen_path = f"{self.outdir}/{file_name}.cif"
|
| 312 |
+
|
| 313 |
+
# design mask bfactor
|
| 314 |
+
design_mask = batch["design_mask"][0].float()
|
| 315 |
+
atom_design_mask = (
|
| 316 |
+
sample["atom_to_token"].float() @ design_mask.unsqueeze(-1).float()
|
| 317 |
+
)
|
| 318 |
+
design_mask = native["design_mask"].float()
|
| 319 |
+
|
| 320 |
+
atom_design_mask = atom_design_mask.squeeze().bool()
|
| 321 |
+
bfactor = atom_design_mask * 100
|
| 322 |
+
|
| 323 |
+
# binding type bfactor
|
| 324 |
+
binding_type = batch["binding_type"][0].float()
|
| 325 |
+
atom_binding_type = (
|
| 326 |
+
sample["atom_to_token"].float() @ binding_type.unsqueeze(-1).float()
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
atom_binding_type = atom_binding_type.squeeze().int()
|
| 330 |
+
bfactor[atom_binding_type == const.binding_type_ids["BINDING"]] = 60
|
| 331 |
+
|
| 332 |
+
bfactor = atom_design_mask[sample["atom_pad_mask"].bool()].float()
|
| 333 |
+
str_native.atoms["bfactor"] = bfactor.cpu().numpy()
|
| 334 |
+
structure.atoms["bfactor"] = bfactor.cpu().numpy()
|
| 335 |
+
|
| 336 |
+
# Add dummy (0-coord) design side chains if inverse fold
|
| 337 |
+
if self.inverse_fold:
|
| 338 |
+
atom_design_mask_no_pad = atom_design_mask[
|
| 339 |
+
native["atom_pad_mask"].bool()
|
| 340 |
+
]
|
| 341 |
+
res_design_mask = np.array(
|
| 342 |
+
[
|
| 343 |
+
all(
|
| 344 |
+
atom_design_mask_no_pad[
|
| 345 |
+
res["atom_idx"] : res["atom_idx"] + res["atom_num"]
|
| 346 |
+
]
|
| 347 |
+
)
|
| 348 |
+
for res in structure.residues
|
| 349 |
+
]
|
| 350 |
+
)
|
| 351 |
+
structure = Structure.add_side_chains(
|
| 352 |
+
structure, residue_mask=res_design_mask
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
if self.write_native:
|
| 356 |
+
open(native_path, "w").write(to_mmcif(str_native))
|
| 357 |
+
|
| 358 |
+
pred_binding_mask = prediction["binding_type"][0].cpu().bool().numpy()
|
| 359 |
+
if self.design:
|
| 360 |
+
chain_design_mask = (
|
| 361 |
+
prediction["chain_design_mask"][0].cpu().bool().numpy()
|
| 362 |
+
)
|
| 363 |
+
pred_design_mask = prediction["design_mask"][0].cpu().bool().numpy()
|
| 364 |
+
design_color_features = np.ones_like(pred_binding_mask) * 0.8
|
| 365 |
+
design_color_features[pred_binding_mask] = 1.0
|
| 366 |
+
if self.design:
|
| 367 |
+
design_color_features[chain_design_mask] = 0.0
|
| 368 |
+
design_color_features[pred_design_mask] = 0.6
|
| 369 |
+
|
| 370 |
+
# Create a mask to identify unique token-to-res mappings.
|
| 371 |
+
# This is for small molecules where multiple tokens can be mapped to the same residue.
|
| 372 |
+
token_to_res = prediction["token_to_res"][0].cpu().numpy()
|
| 373 |
+
unique_mask = np.ones_like(token_to_res, dtype=bool)
|
| 374 |
+
unique_mask[1:] = token_to_res[1:] != token_to_res[:-1]
|
| 375 |
+
design_color_features = design_color_features[unique_mask]
|
| 376 |
+
open(gen_path, "w").write(
|
| 377 |
+
to_mmcif(
|
| 378 |
+
structure,
|
| 379 |
+
design_coloring=True,
|
| 380 |
+
color_features=design_color_features,
|
| 381 |
+
)
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
# Write metadata
|
| 385 |
+
metadata_path = f"{self.outdir}/{file_name}.npz"
|
| 386 |
+
token_mask = sample["token_pad_mask"].bool()
|
| 387 |
+
|
| 388 |
+
# Build metadata dict with required fields
|
| 389 |
+
metadata_dict = {
|
| 390 |
+
"design_mask": design_mask[token_mask].cpu().numpy(),
|
| 391 |
+
"mol_type": sample["mol_type"][token_mask].cpu().numpy(),
|
| 392 |
+
"ss_type": sample["ss_type"][token_mask].cpu().numpy(),
|
| 393 |
+
"token_resolved_mask": sample["token_resolved_mask"][token_mask].cpu().numpy(),
|
| 394 |
+
"binding_type": binding_type[token_mask].cpu().numpy(),
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
# Add optional fields only if they have valid values (avoid None -> object array)
|
| 398 |
+
if "inverse_fold_design_mask" in sample:
|
| 399 |
+
metadata_dict["inverse_fold_design_mask"] = (
|
| 400 |
+
sample["inverse_fold_design_mask"][token_mask].cpu().numpy()
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
# Per-residue amino acid constraints (for inverse folding step)
|
| 404 |
+
# Only save if constraints exist AND have non-zero values
|
| 405 |
+
if "aa_constraint_mask" in batch:
|
| 406 |
+
aa_mask = batch["aa_constraint_mask"][0]
|
| 407 |
+
if aa_mask.any(): # Only save if there are actual constraints
|
| 408 |
+
metadata_dict["aa_constraint_mask"] = aa_mask[token_mask].cpu().numpy()
|
| 409 |
+
|
| 410 |
+
np.savez_compressed(metadata_path, **metadata_dict)
|
| 411 |
+
|
| 412 |
+
# Write trajectories
|
| 413 |
+
if self.save_traj:
|
| 414 |
+
trajs = torch.stack(prediction["coords_traj"], dim=1)
|
| 415 |
+
traj = trajs[n]
|
| 416 |
+
aligned = [traj[0]]
|
| 417 |
+
for frame in traj[1:]:
|
| 418 |
+
with torch.autocast("cuda", enabled=False):
|
| 419 |
+
aligned.append(
|
| 420 |
+
weighted_rigid_align(
|
| 421 |
+
frame.float().unsqueeze(0),
|
| 422 |
+
aligned[-1].float().unsqueeze(0),
|
| 423 |
+
sample["atom_pad_mask"].float().unsqueeze(0),
|
| 424 |
+
sample["atom_pad_mask"].float().unsqueeze(0),
|
| 425 |
+
)
|
| 426 |
+
.to(frame)
|
| 427 |
+
.squeeze()
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
pdbs = []
|
| 431 |
+
all_coords = []
|
| 432 |
+
ensemble = []
|
| 433 |
+
atom_idx = 0
|
| 434 |
+
for idx, frame in tqdm(
|
| 435 |
+
enumerate(aligned), desc="Writing traj.", total=len(aligned)
|
| 436 |
+
):
|
| 437 |
+
sample["coords"] = frame
|
| 438 |
+
if self.atom14:
|
| 439 |
+
sample = res_from_atom14(sample)
|
| 440 |
+
elif self.atom37:
|
| 441 |
+
sample = res_from_atom37(sample)
|
| 442 |
+
else:
|
| 443 |
+
raise ValueError("Either atom14 or atom37 must be true")
|
| 444 |
+
|
| 445 |
+
str_frame, _, _ = Structure.from_feat(sample)
|
| 446 |
+
pdbs.append(to_pdb(str_frame))
|
| 447 |
+
all_coords.append(str_frame.coords)
|
| 448 |
+
ensemble.append(
|
| 449 |
+
(
|
| 450 |
+
atom_idx,
|
| 451 |
+
len(str_frame.coords),
|
| 452 |
+
)
|
| 453 |
+
)
|
| 454 |
+
atom_idx += len(str_frame.coords)
|
| 455 |
+
|
| 456 |
+
open(self.outdir / f"{file_name}_traj.pdb", "w").write(
|
| 457 |
+
self.combine_pdb_models(pdbs)
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Write x0 trajectories
|
| 461 |
+
if self.save_x0_traj:
|
| 462 |
+
trajs = torch.stack(prediction["x0_coords_traj"], dim=1)
|
| 463 |
+
traj = trajs[n]
|
| 464 |
+
aligned = [traj[0]]
|
| 465 |
+
for frame in traj[1:]:
|
| 466 |
+
with torch.autocast("cuda", enabled=False):
|
| 467 |
+
aligned.append(
|
| 468 |
+
weighted_rigid_align(
|
| 469 |
+
frame.float().unsqueeze(0),
|
| 470 |
+
aligned[-1].float().unsqueeze(0),
|
| 471 |
+
sample["atom_pad_mask"].float().unsqueeze(0),
|
| 472 |
+
sample["atom_pad_mask"].float().unsqueeze(0),
|
| 473 |
+
)
|
| 474 |
+
.to(frame)
|
| 475 |
+
.squeeze()
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
pdbs = []
|
| 479 |
+
all_coords = []
|
| 480 |
+
ensemble = []
|
| 481 |
+
atom_idx = 0
|
| 482 |
+
for idx, frame in tqdm(
|
| 483 |
+
enumerate(aligned), desc="Writing x0 traj.", total=len(aligned)
|
| 484 |
+
):
|
| 485 |
+
sample["coords"] = frame
|
| 486 |
+
if self.atom14:
|
| 487 |
+
sample = res_from_atom14(sample)
|
| 488 |
+
elif self.atom37:
|
| 489 |
+
sample = res_from_atom37(sample)
|
| 490 |
+
else:
|
| 491 |
+
raise ValueError("Either atom14 or atom37 must be true")
|
| 492 |
+
|
| 493 |
+
str_frame, _, _ = Structure.from_feat(sample)
|
| 494 |
+
pdbs.append(to_pdb(str_frame))
|
| 495 |
+
all_coords.append(str_frame.coords)
|
| 496 |
+
ensemble.append(
|
| 497 |
+
(
|
| 498 |
+
atom_idx,
|
| 499 |
+
len(str_frame.coords),
|
| 500 |
+
)
|
| 501 |
+
)
|
| 502 |
+
atom_idx += len(str_frame.coords)
|
| 503 |
+
|
| 504 |
+
open(self.outdir / f"{file_name}_x0_traj.pdb", "w").write(
|
| 505 |
+
self.combine_pdb_models(pdbs)
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
except Exception as e: # noqa: BLE001
|
| 509 |
+
import traceback
|
| 510 |
+
|
| 511 |
+
traceback.print_exc() # noqa: T201
|
| 512 |
+
msg = f"predict/writer.py: Validation structure writing failed on {batch['id'][0]} with error {e}. Skipping."
|
| 513 |
+
print(msg)
|
| 514 |
+
|
| 515 |
+
def combine_pdb_models(self, pdb_strings):
|
| 516 |
+
combined_pdb = ""
|
| 517 |
+
model_number = 1
|
| 518 |
+
|
| 519 |
+
for pdb in pdb_strings:
|
| 520 |
+
# Add a model number at the start of each model
|
| 521 |
+
combined_pdb += f"MODEL {model_number}\n"
|
| 522 |
+
combined_pdb += pdb.split("\nEND")[0]
|
| 523 |
+
combined_pdb += "\nENDMDL\n" # End of model marker
|
| 524 |
+
model_number += 1
|
| 525 |
+
|
| 526 |
+
return combined_pdb
|
| 527 |
+
|
| 528 |
+
def on_predict_epoch_end(
|
| 529 |
+
self,
|
| 530 |
+
trainer: Trainer, # noqa: ARG002
|
| 531 |
+
pl_module: LightningModule, # noqa: ARG002
|
| 532 |
+
) -> None:
|
| 533 |
+
"""Print the number of failed examples."""
|
| 534 |
+
print(f"Number of failed examples: {self.failed}") # noqa: T201
|
model/boltzgen/task/task.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
|
| 3 |
+
from omegaconf import OmegaConf
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Task(ABC):
|
| 7 |
+
"""A task to be executed."""
|
| 8 |
+
|
| 9 |
+
@abstractmethod
|
| 10 |
+
def run(self, config: OmegaConf) -> None:
|
| 11 |
+
"""Run the task.
|
| 12 |
+
|
| 13 |
+
Parameters
|
| 14 |
+
----------
|
| 15 |
+
config : OmegaConf
|
| 16 |
+
The configuration for the task.
|
| 17 |
+
|
| 18 |
+
"""
|
| 19 |
+
raise NotImplementedError
|
model/boltzgen/task/train/__init__.py
ADDED
|
File without changes
|
model/boltzgen/task/train/data.py
ADDED
|
@@ -0,0 +1,1259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import defaultdict
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import traceback
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import pytorch_lightning as pl
|
| 10 |
+
import torch
|
| 11 |
+
from rdkit.Chem import Mol
|
| 12 |
+
from torch import Tensor
|
| 13 |
+
from torch.utils.data import DataLoader
|
| 14 |
+
|
| 15 |
+
from boltzgen.data import const
|
| 16 |
+
from boltzgen.data.crop.cropper import Cropper
|
| 17 |
+
from boltzgen.data.select.selector import Selector
|
| 18 |
+
from boltzgen.data.data import (
|
| 19 |
+
MSA,
|
| 20 |
+
Input,
|
| 21 |
+
Manifest,
|
| 22 |
+
Record,
|
| 23 |
+
Structure,
|
| 24 |
+
)
|
| 25 |
+
from boltzgen.data.feature.featurizer import Featurizer
|
| 26 |
+
from boltzgen.data.filter.dynamic.filter import DynamicFilter
|
| 27 |
+
from boltzgen.data.mol import load_canonicals, load_molecules
|
| 28 |
+
from boltzgen.data.pad import pad_to_max
|
| 29 |
+
from boltzgen.data.sample.sampler import Sample, Sampler
|
| 30 |
+
from boltzgen.data.template.features import load_dummy_templates
|
| 31 |
+
from boltzgen.data.tokenize.tokenizer import Tokenizer
|
| 32 |
+
from boltzgen.task.predict import data_ligands, data_protein_binder
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class DatasetConfig:
|
| 37 |
+
"""Dataset configuration."""
|
| 38 |
+
|
| 39 |
+
target_dir: str
|
| 40 |
+
msa_dir: str
|
| 41 |
+
prob: Optional[float]
|
| 42 |
+
sampler: Sampler
|
| 43 |
+
cropper: Cropper
|
| 44 |
+
selector: Optional[Selector] = None
|
| 45 |
+
manifest_path: Optional[str] = None
|
| 46 |
+
filters: Optional[list[DynamicFilter]] = None
|
| 47 |
+
split: Optional[str] = None
|
| 48 |
+
symmetry_correction: bool = True
|
| 49 |
+
val_group: Optional[str] = "RCSB"
|
| 50 |
+
use_train_subset: Optional[float] = None
|
| 51 |
+
moldir: Optional[str] = None
|
| 52 |
+
override_bfactor: Optional[bool] = False
|
| 53 |
+
override_method: Optional[str] = None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class DataConfig:
|
| 58 |
+
"""Data configuration."""
|
| 59 |
+
|
| 60 |
+
datasets: List[DatasetConfig]
|
| 61 |
+
featurizer: Featurizer
|
| 62 |
+
tokenizer: Tokenizer
|
| 63 |
+
selector: Selector
|
| 64 |
+
max_atoms: int
|
| 65 |
+
max_tokens: int
|
| 66 |
+
max_seqs: int
|
| 67 |
+
samples_per_epoch: int
|
| 68 |
+
batch_size: int
|
| 69 |
+
num_workers: int
|
| 70 |
+
random_seed: int
|
| 71 |
+
pin_memory: bool
|
| 72 |
+
atoms_per_window_queries: int
|
| 73 |
+
min_dist: float
|
| 74 |
+
max_dist: float
|
| 75 |
+
num_bins: int
|
| 76 |
+
overfit: Optional[int] = None
|
| 77 |
+
pad_to_max_tokens: bool = False
|
| 78 |
+
pad_to_max_atoms: bool = False
|
| 79 |
+
pad_to_max_seqs: bool = False
|
| 80 |
+
return_train_symmetries: bool = False
|
| 81 |
+
return_val_symmetries: bool = True
|
| 82 |
+
val_batch_size: int = 1
|
| 83 |
+
single_sequence_prop_training: float = 0.0
|
| 84 |
+
msa_sampling_training: bool = False
|
| 85 |
+
moldir: Optional[str] = None
|
| 86 |
+
compute_frames: bool = True
|
| 87 |
+
backbone_only: bool = False
|
| 88 |
+
atom14: bool = False
|
| 89 |
+
atom37: bool = False
|
| 90 |
+
design: bool = False
|
| 91 |
+
monomer_split: str = None
|
| 92 |
+
monomer_target_dir: str = None
|
| 93 |
+
monomer_seq_len: int = 100
|
| 94 |
+
monomer_target_structure_condition: bool = True
|
| 95 |
+
inverse_fold: bool = False
|
| 96 |
+
ligand_split: str = None
|
| 97 |
+
ligand_target_dir: str = None
|
| 98 |
+
ligand_seq_len: int = 100
|
| 99 |
+
use_msa: bool = True
|
| 100 |
+
disulfide_prob: float = 1.0
|
| 101 |
+
disulfide_on: bool = False
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@dataclass
|
| 105 |
+
class Dataset:
|
| 106 |
+
"""Data holder."""
|
| 107 |
+
|
| 108 |
+
samples: pd.DataFrame
|
| 109 |
+
struct_dir: Path
|
| 110 |
+
msa_dir: Path
|
| 111 |
+
record_dir: Path
|
| 112 |
+
prob: float
|
| 113 |
+
cropper: Cropper
|
| 114 |
+
tokenizer: Tokenizer
|
| 115 |
+
featurizer: Featurizer
|
| 116 |
+
val_group: str
|
| 117 |
+
selector: Selector
|
| 118 |
+
symmetry_correction: bool = True
|
| 119 |
+
moldir: Optional[str] = None
|
| 120 |
+
override_bfactor: Optional[bool] = False
|
| 121 |
+
override_method: Optional[str] = None
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def load_record(record_id: str, record_dir: Path) -> Record:
|
| 125 |
+
"""Load the given record.
|
| 126 |
+
|
| 127 |
+
Parameters
|
| 128 |
+
----------
|
| 129 |
+
record_id : str
|
| 130 |
+
The record id to load.
|
| 131 |
+
record_dir : Path
|
| 132 |
+
The path to the record directory.
|
| 133 |
+
|
| 134 |
+
Returns
|
| 135 |
+
-------
|
| 136 |
+
Record
|
| 137 |
+
The loaded record.
|
| 138 |
+
"""
|
| 139 |
+
return Record.load(record_dir / f"{record_id}.json")
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def load_structure(record: Record, struct_dir: Path) -> Structure:
|
| 143 |
+
"""Load the given input data.
|
| 144 |
+
|
| 145 |
+
Parameters
|
| 146 |
+
----------
|
| 147 |
+
record : str
|
| 148 |
+
The record to load.
|
| 149 |
+
target_dir : Path
|
| 150 |
+
The path to the data directory.
|
| 151 |
+
|
| 152 |
+
Returns
|
| 153 |
+
-------
|
| 154 |
+
Input
|
| 155 |
+
The loaded input.
|
| 156 |
+
|
| 157 |
+
"""
|
| 158 |
+
if (struct_dir / f"{record.id}.npz").exists():
|
| 159 |
+
structure_path = struct_dir / f"{record.id}.npz"
|
| 160 |
+
else:
|
| 161 |
+
structure_path = struct_dir / f"{record.id}" / f"{record.id}_model_0.npz"
|
| 162 |
+
return Structure.load(structure_path)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def load_msas(chain_ids: set[int], record: Record, msa_dir: Path) -> Input:
|
| 166 |
+
"""Load the given input data.
|
| 167 |
+
|
| 168 |
+
Parameters
|
| 169 |
+
----------
|
| 170 |
+
chain_ids : set[int]
|
| 171 |
+
The chain ids to load.
|
| 172 |
+
record : Record
|
| 173 |
+
The record to load.
|
| 174 |
+
msa_dir : Path
|
| 175 |
+
The path to the MSA directory.
|
| 176 |
+
|
| 177 |
+
Returns
|
| 178 |
+
-------
|
| 179 |
+
Input
|
| 180 |
+
The loaded input.
|
| 181 |
+
|
| 182 |
+
"""
|
| 183 |
+
msas = {}
|
| 184 |
+
for chain in record.chains:
|
| 185 |
+
if chain.chain_id not in chain_ids:
|
| 186 |
+
continue
|
| 187 |
+
|
| 188 |
+
msa_id = chain.msa_id
|
| 189 |
+
if msa_id != -1:
|
| 190 |
+
msa_path = msa_dir / f"{msa_id}.npz"
|
| 191 |
+
msa = MSA.load(msa_path)
|
| 192 |
+
msas[chain.chain_id] = msa
|
| 193 |
+
|
| 194 |
+
return msas
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def collate(data: List[Dict[str, Tensor]]) -> Dict[str, Tensor]:
|
| 198 |
+
"""Collate the data.
|
| 199 |
+
|
| 200 |
+
Parameters
|
| 201 |
+
----------
|
| 202 |
+
data : List[Dict[str, Tensor]]
|
| 203 |
+
The data to collate.
|
| 204 |
+
|
| 205 |
+
Returns
|
| 206 |
+
-------
|
| 207 |
+
Dict[str, Tensor]
|
| 208 |
+
The collated data.
|
| 209 |
+
|
| 210 |
+
"""
|
| 211 |
+
# Get the keys
|
| 212 |
+
keys = data[0].keys()
|
| 213 |
+
|
| 214 |
+
# Collate the data
|
| 215 |
+
collated = {}
|
| 216 |
+
for key in keys:
|
| 217 |
+
values = [d[key] for d in data]
|
| 218 |
+
|
| 219 |
+
if key not in [
|
| 220 |
+
"all_coords",
|
| 221 |
+
"all_resolved_mask",
|
| 222 |
+
"crop_to_all_atom_map",
|
| 223 |
+
"chain_symmetries",
|
| 224 |
+
"chain_swaps",
|
| 225 |
+
"amino_acids_symmetries",
|
| 226 |
+
"ligand_symmetries",
|
| 227 |
+
"activity_name",
|
| 228 |
+
"activity_qualifier",
|
| 229 |
+
"sid",
|
| 230 |
+
"cid",
|
| 231 |
+
"normalized_protein_accession",
|
| 232 |
+
"pair_id",
|
| 233 |
+
"ligand_edge_index",
|
| 234 |
+
"ligand_edge_lower_bounds",
|
| 235 |
+
"ligand_edge_upper_bounds",
|
| 236 |
+
"ligand_edge_bond_mask",
|
| 237 |
+
"ligand_edge_angle_mask",
|
| 238 |
+
"connections_edge_index",
|
| 239 |
+
"ligand_chiral_atom_index",
|
| 240 |
+
"ligand_chiral_check_mask",
|
| 241 |
+
"ligand_chiral_atom_orientations",
|
| 242 |
+
"ligand_stereo_bond_index",
|
| 243 |
+
"ligand_stereo_check_mask",
|
| 244 |
+
"ligand_stereo_bond_orientations",
|
| 245 |
+
"ligand_aromatic_5_ring_index",
|
| 246 |
+
"ligand_aromatic_6_ring_index",
|
| 247 |
+
"ligand_planar_double_bond_index",
|
| 248 |
+
"pdb_id",
|
| 249 |
+
"id",
|
| 250 |
+
"structure_bonds",
|
| 251 |
+
"extra_mols",
|
| 252 |
+
]:
|
| 253 |
+
if values[0] is not None:
|
| 254 |
+
# Check if all have the same shape
|
| 255 |
+
shape = values[0].shape
|
| 256 |
+
if not all(v.shape == shape for v in values):
|
| 257 |
+
values = pad_to_max(values, 0)
|
| 258 |
+
else:
|
| 259 |
+
values = torch.stack(values, dim=0)
|
| 260 |
+
|
| 261 |
+
# Stack the values
|
| 262 |
+
collated[key] = values
|
| 263 |
+
|
| 264 |
+
return collated
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class TrainingDataset(torch.utils.data.Dataset):
|
| 268 |
+
"""Base iterable dataset."""
|
| 269 |
+
|
| 270 |
+
def __init__(
|
| 271 |
+
self,
|
| 272 |
+
datasets: List[Dataset],
|
| 273 |
+
canonicals: dict[str, Mol],
|
| 274 |
+
moldir: str,
|
| 275 |
+
samples_per_epoch: int,
|
| 276 |
+
max_atoms: int,
|
| 277 |
+
max_tokens: int,
|
| 278 |
+
max_seqs: int,
|
| 279 |
+
pad_to_max_atoms: bool = False,
|
| 280 |
+
pad_to_max_tokens: bool = False,
|
| 281 |
+
pad_to_max_seqs: bool = False,
|
| 282 |
+
atoms_per_window_queries: int = 32,
|
| 283 |
+
min_dist: float = 2.0,
|
| 284 |
+
max_dist: float = 22.0,
|
| 285 |
+
num_bins: int = 64,
|
| 286 |
+
overfit: Optional[int] = None,
|
| 287 |
+
return_symmetries: Optional[bool] = False,
|
| 288 |
+
single_sequence_prop: Optional[float] = 0.0,
|
| 289 |
+
msa_sampling: bool = False,
|
| 290 |
+
compute_frames: bool = True,
|
| 291 |
+
backbone_only: bool = False,
|
| 292 |
+
atom14: bool = False,
|
| 293 |
+
atom37: bool = False,
|
| 294 |
+
design: bool = False,
|
| 295 |
+
disulfide_prob: float = 1.0,
|
| 296 |
+
disulfide_on: bool = False,
|
| 297 |
+
use_msa: bool = True,
|
| 298 |
+
inverse_fold: bool = False,
|
| 299 |
+
) -> None:
|
| 300 |
+
"""Initialize the training dataset.
|
| 301 |
+
|
| 302 |
+
Parameters
|
| 303 |
+
----------
|
| 304 |
+
datasets : List[Dataset]
|
| 305 |
+
The datasets to sample from.
|
| 306 |
+
samplers : List[Sampler]
|
| 307 |
+
The samplers to sample from each dataset.
|
| 308 |
+
probs : List[float]
|
| 309 |
+
The probabilities to sample from each dataset.
|
| 310 |
+
samples_per_epoch : int
|
| 311 |
+
The number of samples per epoch.
|
| 312 |
+
max_tokens : int
|
| 313 |
+
The maximum number of tokens.
|
| 314 |
+
|
| 315 |
+
"""
|
| 316 |
+
super().__init__()
|
| 317 |
+
self.datasets = datasets
|
| 318 |
+
self.canonicals = canonicals
|
| 319 |
+
self.moldir = moldir
|
| 320 |
+
self.probs = [d.prob for d in datasets]
|
| 321 |
+
self.samples_per_epoch = samples_per_epoch
|
| 322 |
+
self.max_tokens = max_tokens
|
| 323 |
+
self.max_seqs = max_seqs
|
| 324 |
+
self.max_atoms = max_atoms
|
| 325 |
+
self.pad_to_max_tokens = pad_to_max_tokens
|
| 326 |
+
self.pad_to_max_atoms = pad_to_max_atoms
|
| 327 |
+
self.pad_to_max_seqs = pad_to_max_seqs
|
| 328 |
+
self.atoms_per_window_queries = atoms_per_window_queries
|
| 329 |
+
self.min_dist = min_dist
|
| 330 |
+
self.max_dist = max_dist
|
| 331 |
+
self.num_bins = num_bins
|
| 332 |
+
self.return_symmetries = return_symmetries
|
| 333 |
+
self.backbone_only = backbone_only
|
| 334 |
+
self.atom14 = atom14
|
| 335 |
+
self.atom37 = atom37
|
| 336 |
+
self.design = design
|
| 337 |
+
self.disulfide_prob = disulfide_prob
|
| 338 |
+
self.disulfide_on = disulfide_on
|
| 339 |
+
self.single_sequence_prop = single_sequence_prop
|
| 340 |
+
self.msa_sampling = msa_sampling
|
| 341 |
+
self.use_msa = use_msa
|
| 342 |
+
self.overfit = overfit
|
| 343 |
+
self.compute_frames = compute_frames
|
| 344 |
+
self.inverse_fold = inverse_fold
|
| 345 |
+
|
| 346 |
+
self.samples: list[list[Dict]] = []
|
| 347 |
+
self.samples_weight: list[list[float]] = []
|
| 348 |
+
for d in self.datasets:
|
| 349 |
+
if self.overfit:
|
| 350 |
+
samples = d.samples[: self.overfit]
|
| 351 |
+
else:
|
| 352 |
+
samples = d.samples
|
| 353 |
+
self.samples.append(
|
| 354 |
+
[
|
| 355 |
+
samples.iloc[sample_idx].to_dict()
|
| 356 |
+
for sample_idx in range(len(samples))
|
| 357 |
+
]
|
| 358 |
+
)
|
| 359 |
+
self.samples_weight.append(samples["weight"].tolist())
|
| 360 |
+
|
| 361 |
+
def __getitem__(self, idx: int) -> Dict[str, Tensor]:
|
| 362 |
+
"""Get an item from the dataset.
|
| 363 |
+
|
| 364 |
+
Returns
|
| 365 |
+
-------
|
| 366 |
+
Dict[str, Tensor]
|
| 367 |
+
The sampled data features.
|
| 368 |
+
|
| 369 |
+
"""
|
| 370 |
+
# Set a random state
|
| 371 |
+
random = np.random.default_rng()
|
| 372 |
+
|
| 373 |
+
# Pick a random dataset
|
| 374 |
+
dataset_idx = random.choice(len(self.datasets), p=self.probs)
|
| 375 |
+
|
| 376 |
+
dataset = self.datasets[dataset_idx]
|
| 377 |
+
|
| 378 |
+
# Get a sample from the dataset
|
| 379 |
+
samples = self.samples[dataset_idx]
|
| 380 |
+
sample_idx = random.choice(
|
| 381 |
+
len(samples),
|
| 382 |
+
p=(
|
| 383 |
+
self.samples_weight[dataset_idx]
|
| 384 |
+
/ np.sum(self.samples_weight[dataset_idx])
|
| 385 |
+
if self.overfit
|
| 386 |
+
else self.samples_weight[dataset_idx]
|
| 387 |
+
),
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
sample = samples[sample_idx]
|
| 391 |
+
sample: Sample = Sample(
|
| 392 |
+
record_id=str(sample["record_id"]),
|
| 393 |
+
chain_id=(
|
| 394 |
+
int(sample["chain_id"]) if sample["chain_id"] is not None else None
|
| 395 |
+
),
|
| 396 |
+
interface_id=(
|
| 397 |
+
int(sample["interface_id"])
|
| 398 |
+
if sample["interface_id"] is not None
|
| 399 |
+
else None
|
| 400 |
+
),
|
| 401 |
+
weight=float(sample["weight"]),
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
# Load record
|
| 405 |
+
record = load_record(sample.record_id, dataset.record_dir)
|
| 406 |
+
|
| 407 |
+
# Get the structure
|
| 408 |
+
try:
|
| 409 |
+
structure = load_structure(record, dataset.struct_dir)
|
| 410 |
+
except Exception as e: # noqa: BLE001
|
| 411 |
+
print(f"Failed to load input for {record.id} with error {e}. Skipping.")
|
| 412 |
+
return self.__getitem__(random.integers(0, len(self)))
|
| 413 |
+
|
| 414 |
+
# Tokenize structure
|
| 415 |
+
try:
|
| 416 |
+
tokenized = dataset.tokenizer.tokenize(
|
| 417 |
+
structure, inverse_fold=self.inverse_fold
|
| 418 |
+
)
|
| 419 |
+
except Exception as e: # noqa: BLE001
|
| 420 |
+
print(f"Tokenizer failed on {record.id} with error {e}. Skipping.")
|
| 421 |
+
traceback.print_exc() # noqa: T201
|
| 422 |
+
return self.__getitem__(random.integers(0, len(self)))
|
| 423 |
+
|
| 424 |
+
# Compute crop
|
| 425 |
+
try:
|
| 426 |
+
if self.max_tokens is not None and len(tokenized.tokens) > self.max_tokens:
|
| 427 |
+
tokenized = dataset.cropper.crop(
|
| 428 |
+
tokenized,
|
| 429 |
+
max_atoms=self.max_atoms,
|
| 430 |
+
max_tokens=self.max_tokens,
|
| 431 |
+
chain_id=sample.chain_id,
|
| 432 |
+
interface_id=sample.interface_id,
|
| 433 |
+
random=random,
|
| 434 |
+
prefer_protein_queries=self.inverse_fold,
|
| 435 |
+
)
|
| 436 |
+
if len(tokenized.tokens) == 0:
|
| 437 |
+
msg = "No tokens in cropped structure."
|
| 438 |
+
raise ValueError(msg) # noqa: TRY301
|
| 439 |
+
except Exception as e: # noqa: BLE001
|
| 440 |
+
print(f"Cropper failed on {record.id} with error {e}. Skipping.")
|
| 441 |
+
traceback.print_exc() # noqa: T201
|
| 442 |
+
return self.__getitem__(random.integers(0, len(self)))
|
| 443 |
+
|
| 444 |
+
# Select which tokens to design
|
| 445 |
+
try:
|
| 446 |
+
tokenized, design_task = dataset.selector.select(
|
| 447 |
+
tokenized,
|
| 448 |
+
random=random,
|
| 449 |
+
)
|
| 450 |
+
except Exception as e: # noqa: BLE001
|
| 451 |
+
print(f"Selector failed on {record.id} with error {e}. Skipping.") # noqa: T201
|
| 452 |
+
traceback.print_exc() # noqa: T201
|
| 453 |
+
return self.__getitem__(random.integers(0, len(self)))
|
| 454 |
+
structure = tokenized.structure
|
| 455 |
+
|
| 456 |
+
# Get unique chain ids
|
| 457 |
+
chain_ids = set(tokenized.tokens["asym_id"])
|
| 458 |
+
|
| 459 |
+
# Load msas and templates
|
| 460 |
+
try:
|
| 461 |
+
if self.use_msa:
|
| 462 |
+
msas = load_msas(
|
| 463 |
+
chain_ids=chain_ids,
|
| 464 |
+
record=record,
|
| 465 |
+
msa_dir=dataset.msa_dir,
|
| 466 |
+
)
|
| 467 |
+
else:
|
| 468 |
+
msas = {}
|
| 469 |
+
except Exception as e: # noqa: BLE001
|
| 470 |
+
print(f"MSA loading failed for {record.id} with error {e}. Skipping.")
|
| 471 |
+
return self.__getitem__(random.integers(0, len(self)))
|
| 472 |
+
|
| 473 |
+
# Load molecules
|
| 474 |
+
try:
|
| 475 |
+
# Try to find molecules in the dataset moldir if provided
|
| 476 |
+
# Find missing ones in global moldir and check if all found
|
| 477 |
+
molecules = {}
|
| 478 |
+
molecules.update(self.canonicals)
|
| 479 |
+
mol_names = set(tokenized.tokens["res_name"].tolist())
|
| 480 |
+
mol_names = mol_names - set(self.canonicals.keys())
|
| 481 |
+
if dataset.moldir is not None:
|
| 482 |
+
molecules.update(load_molecules(dataset.moldir, mol_names))
|
| 483 |
+
|
| 484 |
+
mol_names = mol_names - set(molecules.keys())
|
| 485 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 486 |
+
except Exception as e: # noqa: BLE001
|
| 487 |
+
print(f"Molecule loading failed for {record.id} with error {e}. Skipping.")
|
| 488 |
+
return self.__getitem__(random.integers(0, len(self)))
|
| 489 |
+
|
| 490 |
+
# Finalize input data
|
| 491 |
+
input_data = Input(
|
| 492 |
+
tokens=tokenized.tokens,
|
| 493 |
+
bonds=tokenized.bonds,
|
| 494 |
+
token_to_res=tokenized.token_to_res,
|
| 495 |
+
structure=tokenized.structure,
|
| 496 |
+
msa=msas,
|
| 497 |
+
templates=None,
|
| 498 |
+
record=record,
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
# Compute features
|
| 502 |
+
try:
|
| 503 |
+
features: dict = dataset.featurizer.process(
|
| 504 |
+
input_data,
|
| 505 |
+
molecules=molecules,
|
| 506 |
+
random=random,
|
| 507 |
+
training=True,
|
| 508 |
+
max_atoms=self.max_atoms if self.pad_to_max_atoms else None,
|
| 509 |
+
max_tokens=self.max_tokens if self.pad_to_max_tokens else None,
|
| 510 |
+
max_seqs=self.max_seqs,
|
| 511 |
+
pad_to_max_seqs=self.pad_to_max_seqs,
|
| 512 |
+
atoms_per_window_queries=self.atoms_per_window_queries,
|
| 513 |
+
min_dist=self.min_dist,
|
| 514 |
+
max_dist=self.max_dist,
|
| 515 |
+
num_bins=self.num_bins,
|
| 516 |
+
compute_symmetries=self.return_symmetries,
|
| 517 |
+
single_sequence_prop=self.single_sequence_prop,
|
| 518 |
+
msa_sampling=self.msa_sampling,
|
| 519 |
+
override_bfactor=dataset.override_bfactor,
|
| 520 |
+
override_method=dataset.override_method,
|
| 521 |
+
compute_frames=self.compute_frames,
|
| 522 |
+
backbone_only=self.backbone_only,
|
| 523 |
+
atom14=self.atom14,
|
| 524 |
+
atom37=self.atom37,
|
| 525 |
+
design=self.design,
|
| 526 |
+
disulfide_prob=self.disulfide_prob,
|
| 527 |
+
inverse_fold=self.inverse_fold,
|
| 528 |
+
)
|
| 529 |
+
except Exception as e: # noqa: BLE001
|
| 530 |
+
print(f"Featurizer failed on {record.id} with error {e}. Skipping.")
|
| 531 |
+
traceback.print_exc()
|
| 532 |
+
return self.__getitem__(random.integers(0, len(self)))
|
| 533 |
+
|
| 534 |
+
# Check that there is enough stuff to design in the inverse folding case so we have no nan losses
|
| 535 |
+
if self.inverse_fold and features["design_mask"].sum() < 3:
|
| 536 |
+
print(f"Skipping {record.id}. Fewer than 3 design residues.")
|
| 537 |
+
return self.__getitem__(random.integers(0, len(self)))
|
| 538 |
+
|
| 539 |
+
# Set template features
|
| 540 |
+
template_features = load_dummy_templates(
|
| 541 |
+
tdim=1, num_tokens=len(features["res_type"])
|
| 542 |
+
)
|
| 543 |
+
features.update(template_features)
|
| 544 |
+
|
| 545 |
+
features.update({"id": sample.record_id})
|
| 546 |
+
features["pdb_id"] = record.id
|
| 547 |
+
|
| 548 |
+
# Assert that all design tokens make sense
|
| 549 |
+
bad_protein_mask = (
|
| 550 |
+
(~features["is_standard"].bool())
|
| 551 |
+
& features["design_mask"].bool()
|
| 552 |
+
& (features["mol_type"] == const.chain_type_ids["PROTEIN"])
|
| 553 |
+
)
|
| 554 |
+
assert not bad_protein_mask.any()
|
| 555 |
+
|
| 556 |
+
return features
|
| 557 |
+
|
| 558 |
+
def __len__(self) -> int:
|
| 559 |
+
"""Get the length of the dataset.
|
| 560 |
+
|
| 561 |
+
Returns
|
| 562 |
+
-------
|
| 563 |
+
int
|
| 564 |
+
The length of the dataset.
|
| 565 |
+
|
| 566 |
+
"""
|
| 567 |
+
return self.samples_per_epoch
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
class ValidationDataset(torch.utils.data.Dataset):
|
| 571 |
+
"""Base iterable dataset."""
|
| 572 |
+
|
| 573 |
+
def __init__(
|
| 574 |
+
self,
|
| 575 |
+
datasets: List[Dataset],
|
| 576 |
+
canonicals: dict[str, Mol],
|
| 577 |
+
moldir: str,
|
| 578 |
+
seed: int,
|
| 579 |
+
max_atoms: Optional[int] = None,
|
| 580 |
+
max_tokens: Optional[int] = None,
|
| 581 |
+
max_seqs: Optional[int] = None,
|
| 582 |
+
pad_to_max_atoms: bool = False,
|
| 583 |
+
pad_to_max_tokens: bool = False,
|
| 584 |
+
pad_to_max_seqs: bool = False,
|
| 585 |
+
atoms_per_window_queries: int = 32,
|
| 586 |
+
min_dist: float = 2.0,
|
| 587 |
+
max_dist: float = 22.0,
|
| 588 |
+
num_bins: int = 64,
|
| 589 |
+
overfit: Optional[int] = None,
|
| 590 |
+
return_symmetries: Optional[bool] = False,
|
| 591 |
+
compute_frames: bool = True,
|
| 592 |
+
backbone_only: bool = False,
|
| 593 |
+
atom14: bool = False,
|
| 594 |
+
atom37: bool = False,
|
| 595 |
+
design: bool = False,
|
| 596 |
+
inverse_fold: bool = False,
|
| 597 |
+
disulfide_prob: float = 1.0,
|
| 598 |
+
disulfide_on: bool = False,
|
| 599 |
+
) -> None:
|
| 600 |
+
"""Initialize the training dataset.
|
| 601 |
+
|
| 602 |
+
Parameters
|
| 603 |
+
----------
|
| 604 |
+
datasets : List[Dataset]
|
| 605 |
+
The datasets to sample from.
|
| 606 |
+
seed : int
|
| 607 |
+
The random seed.
|
| 608 |
+
max_tokens : int
|
| 609 |
+
The maximum number of tokens.
|
| 610 |
+
overfit : bool
|
| 611 |
+
Whether to overfit the dataset
|
| 612 |
+
|
| 613 |
+
"""
|
| 614 |
+
super().__init__()
|
| 615 |
+
self.datasets = datasets
|
| 616 |
+
self.canonicals = canonicals
|
| 617 |
+
self.moldir = moldir
|
| 618 |
+
self.max_atoms = max_atoms
|
| 619 |
+
self.max_tokens = max_tokens
|
| 620 |
+
self.max_seqs = max_seqs
|
| 621 |
+
self.seed = seed
|
| 622 |
+
self.pad_to_max_tokens = pad_to_max_tokens
|
| 623 |
+
self.pad_to_max_atoms = pad_to_max_atoms
|
| 624 |
+
self.pad_to_max_seqs = pad_to_max_seqs
|
| 625 |
+
self.overfit = overfit
|
| 626 |
+
self.atoms_per_window_queries = atoms_per_window_queries
|
| 627 |
+
self.min_dist = min_dist
|
| 628 |
+
self.max_dist = max_dist
|
| 629 |
+
self.num_bins = num_bins
|
| 630 |
+
self.return_symmetries = return_symmetries
|
| 631 |
+
self.compute_frames = compute_frames
|
| 632 |
+
self.backbone_only = backbone_only
|
| 633 |
+
self.atom14 = atom14
|
| 634 |
+
self.atom37 = atom37
|
| 635 |
+
self.design = design
|
| 636 |
+
self.inverse_fold = inverse_fold
|
| 637 |
+
self.disulfide_prob = disulfide_prob
|
| 638 |
+
self.disulfide_on = disulfide_on
|
| 639 |
+
|
| 640 |
+
def __getitem__(self, idx: int) -> Structure:
|
| 641 |
+
"""Get an item from the dataset.
|
| 642 |
+
|
| 643 |
+
Returns
|
| 644 |
+
-------
|
| 645 |
+
Dict[str, Tensor]
|
| 646 |
+
The sampled data features.
|
| 647 |
+
|
| 648 |
+
"""
|
| 649 |
+
# Set random state
|
| 650 |
+
seed = self.seed if self.overfit is None else None
|
| 651 |
+
random = np.random.default_rng(seed)
|
| 652 |
+
|
| 653 |
+
# Pick dataset based on idx
|
| 654 |
+
for idx_dataset, dataset in enumerate(self.datasets): # noqa: B007
|
| 655 |
+
size = len(dataset.samples)
|
| 656 |
+
if self.overfit is not None:
|
| 657 |
+
size = min(size, self.overfit)
|
| 658 |
+
if idx < size:
|
| 659 |
+
break
|
| 660 |
+
idx -= size
|
| 661 |
+
|
| 662 |
+
# Get a sample from the dataset
|
| 663 |
+
sample = Sample(**dataset.samples.iloc[idx].to_dict())
|
| 664 |
+
record = load_record(sample.record_id, dataset.record_dir)
|
| 665 |
+
|
| 666 |
+
# Get the structure
|
| 667 |
+
try:
|
| 668 |
+
structure = load_structure(record, dataset.struct_dir)
|
| 669 |
+
except Exception as e: # noqa: BLE001
|
| 670 |
+
print(f"Failed to load input for {record.id} with error {e}. Skipping.")
|
| 671 |
+
return self.__getitem__(0)
|
| 672 |
+
|
| 673 |
+
# Tokenize structure
|
| 674 |
+
try:
|
| 675 |
+
tokenized = dataset.tokenizer.tokenize(structure)
|
| 676 |
+
except Exception as e: # noqa: BLE001
|
| 677 |
+
print(f"Tokenizer failed on {record.id} with error {e}. Skipping.") # noqa: T201
|
| 678 |
+
return self.__getitem__(0)
|
| 679 |
+
|
| 680 |
+
# Compute crop
|
| 681 |
+
try:
|
| 682 |
+
if self.max_tokens is not None:
|
| 683 |
+
tokenized = dataset.cropper.crop(
|
| 684 |
+
tokenized,
|
| 685 |
+
max_atoms=self.max_atoms,
|
| 686 |
+
max_tokens=self.max_tokens,
|
| 687 |
+
chain_id=sample.chain_id,
|
| 688 |
+
interface_id=sample.interface_id,
|
| 689 |
+
random=random,
|
| 690 |
+
prefer_protein_queries=self.inverse_fold,
|
| 691 |
+
)
|
| 692 |
+
if len(tokenized.tokens) == 0:
|
| 693 |
+
msg = "No tokens in cropped structure."
|
| 694 |
+
raise ValueError(msg) # noqa: TRY301
|
| 695 |
+
except Exception as e: # noqa: BLE001
|
| 696 |
+
print(f"Cropper failed on {record.id} with error {e}. Skipping.")
|
| 697 |
+
return self.__getitem__(0)
|
| 698 |
+
|
| 699 |
+
# Get unique chains
|
| 700 |
+
chain_ids = set(np.unique(tokenized.tokens["asym_id"]).tolist())
|
| 701 |
+
|
| 702 |
+
# Load msas and templates
|
| 703 |
+
try:
|
| 704 |
+
msas = load_msas(chain_ids, record, dataset.msa_dir)
|
| 705 |
+
except Exception as e: # noqa: BLE001
|
| 706 |
+
print(f"MSA loading failed for {record.id} with error {e}. Skipping.")
|
| 707 |
+
return self.__getitem__(0)
|
| 708 |
+
|
| 709 |
+
# Select which tokens to design
|
| 710 |
+
try:
|
| 711 |
+
tokenized, design_task = dataset.selector.select(
|
| 712 |
+
tokenized,
|
| 713 |
+
random=random,
|
| 714 |
+
)
|
| 715 |
+
except Exception as e: # noqa: BLE001
|
| 716 |
+
print(f"Selector failed on {sample.record_id} with error {e}. Skipping.") # noqa: T201
|
| 717 |
+
traceback.print_exc() # noqa: T201
|
| 718 |
+
return self.__getitem__(0)
|
| 719 |
+
structure = tokenized.structure
|
| 720 |
+
|
| 721 |
+
try:
|
| 722 |
+
# Try to find molecules in the dataset moldir if provided
|
| 723 |
+
# Find missing ones in global moldir and check if all found
|
| 724 |
+
molecules = {}
|
| 725 |
+
molecules.update(self.canonicals)
|
| 726 |
+
mol_names = set(tokenized.tokens["res_name"].tolist())
|
| 727 |
+
mol_names = mol_names - set(self.canonicals.keys())
|
| 728 |
+
if dataset.moldir is not None:
|
| 729 |
+
molecules.update(load_molecules(dataset.moldir, mol_names))
|
| 730 |
+
|
| 731 |
+
mol_names = mol_names - set(molecules.keys())
|
| 732 |
+
molecules.update(load_molecules(self.moldir, mol_names))
|
| 733 |
+
except Exception as e: # noqa: BLE001
|
| 734 |
+
print(f"Molecule loading failed for {record.id} with error {e}. Skipping.")
|
| 735 |
+
return self.__getitem__(0)
|
| 736 |
+
|
| 737 |
+
# Finalize input data
|
| 738 |
+
input_data = Input(
|
| 739 |
+
tokens=tokenized.tokens,
|
| 740 |
+
bonds=tokenized.bonds,
|
| 741 |
+
token_to_res=tokenized.token_to_res,
|
| 742 |
+
structure=tokenized.structure,
|
| 743 |
+
msa=msas,
|
| 744 |
+
templates=None,
|
| 745 |
+
record=record,
|
| 746 |
+
)
|
| 747 |
+
|
| 748 |
+
# Compute features
|
| 749 |
+
try:
|
| 750 |
+
features: dict = dataset.featurizer.process(
|
| 751 |
+
input_data,
|
| 752 |
+
molecules=molecules,
|
| 753 |
+
random=random,
|
| 754 |
+
training=False,
|
| 755 |
+
max_atoms=None,
|
| 756 |
+
max_tokens=None,
|
| 757 |
+
max_seqs=self.max_seqs,
|
| 758 |
+
pad_to_max_seqs=self.pad_to_max_seqs,
|
| 759 |
+
atoms_per_window_queries=self.atoms_per_window_queries,
|
| 760 |
+
min_dist=self.min_dist,
|
| 761 |
+
max_dist=self.max_dist,
|
| 762 |
+
num_bins=self.num_bins,
|
| 763 |
+
compute_symmetries=self.return_symmetries,
|
| 764 |
+
single_sequence_prop=0.0,
|
| 765 |
+
override_method=dataset.override_method,
|
| 766 |
+
compute_frames=self.compute_frames,
|
| 767 |
+
backbone_only=self.backbone_only,
|
| 768 |
+
atom14=self.atom14,
|
| 769 |
+
atom37=self.atom37,
|
| 770 |
+
design=self.design,
|
| 771 |
+
inverse_fold=self.inverse_fold,
|
| 772 |
+
disulfide_prob=self.disulfide_prob,
|
| 773 |
+
disulfide_on=self.disulfide_on,
|
| 774 |
+
)
|
| 775 |
+
except Exception as e: # noqa: BLE001
|
| 776 |
+
print(f"Featurizer failed on {record.id} with error {e}. Skipping.")
|
| 777 |
+
return self.__getitem__(0)
|
| 778 |
+
|
| 779 |
+
# Check that there is enough stuff to design in the inverse folding case so we have no nan losses
|
| 780 |
+
if self.inverse_fold and features["design_mask"].sum() < 3:
|
| 781 |
+
print(f"Skipping {record.id}. Fewer than 3 design residues.")
|
| 782 |
+
return self.__getitem__(0)
|
| 783 |
+
|
| 784 |
+
# Set template features
|
| 785 |
+
template_features = load_dummy_templates(
|
| 786 |
+
tdim=1, num_tokens=len(features["res_type"])
|
| 787 |
+
)
|
| 788 |
+
features.update(template_features)
|
| 789 |
+
|
| 790 |
+
# Add dataset idx
|
| 791 |
+
idx_dataset = torch.tensor([idx_dataset], dtype=torch.long)
|
| 792 |
+
features.update({"idx_dataset": idx_dataset})
|
| 793 |
+
features.update({"id": record.id})
|
| 794 |
+
bad_protein_mask = (
|
| 795 |
+
(~features["is_standard"].bool())
|
| 796 |
+
& features["design_mask"].bool()
|
| 797 |
+
& (features["mol_type"] == const.chain_type_ids["PROTEIN"])
|
| 798 |
+
)
|
| 799 |
+
assert not bad_protein_mask.any()
|
| 800 |
+
return features
|
| 801 |
+
|
| 802 |
+
def __len__(self) -> int:
|
| 803 |
+
"""Get the length of the dataset.
|
| 804 |
+
|
| 805 |
+
Returns
|
| 806 |
+
-------
|
| 807 |
+
int
|
| 808 |
+
The length of the dataaset.
|
| 809 |
+
|
| 810 |
+
"""
|
| 811 |
+
if self.overfit is not None:
|
| 812 |
+
length = sum(len(d.samples[: self.overfit]) for d in self.datasets)
|
| 813 |
+
else:
|
| 814 |
+
length = sum(len(d.samples) for d in self.datasets)
|
| 815 |
+
|
| 816 |
+
return length
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
class TrainingDataModule(pl.LightningDataModule):
|
| 820 |
+
"""DataModule for BoltzGen training."""
|
| 821 |
+
|
| 822 |
+
def __init__(
|
| 823 |
+
self,
|
| 824 |
+
cfg: DataConfig,
|
| 825 |
+
) -> None:
|
| 826 |
+
"""Initialize the DataModule.
|
| 827 |
+
|
| 828 |
+
Parameters
|
| 829 |
+
----------
|
| 830 |
+
config : DataConfig
|
| 831 |
+
The data configuration.
|
| 832 |
+
|
| 833 |
+
"""
|
| 834 |
+
super().__init__()
|
| 835 |
+
self.cfg = cfg
|
| 836 |
+
self.inverse_fold = cfg.inverse_fold
|
| 837 |
+
|
| 838 |
+
assert self.cfg.val_batch_size == 1, "Validation only works with batch size=1."
|
| 839 |
+
|
| 840 |
+
# Load datasets
|
| 841 |
+
train: List[Dataset] = []
|
| 842 |
+
val: List[Dataset] = []
|
| 843 |
+
|
| 844 |
+
for data_config in cfg.datasets:
|
| 845 |
+
# Get relevant directories
|
| 846 |
+
if data_config.manifest_path is not None:
|
| 847 |
+
manifest_path = Path(data_config.manifest_path)
|
| 848 |
+
else:
|
| 849 |
+
manifest_path = Path(data_config.target_dir) / "manifest.json"
|
| 850 |
+
struct_dir = Path(data_config.target_dir) / "structures"
|
| 851 |
+
record_dir = Path(data_config.target_dir) / "records"
|
| 852 |
+
msa_dir = Path(data_config.msa_dir)
|
| 853 |
+
|
| 854 |
+
# Get moldir, if any
|
| 855 |
+
moldir = data_config.moldir
|
| 856 |
+
moldir = Path(moldir) if moldir is not None else None
|
| 857 |
+
|
| 858 |
+
# Load all records
|
| 859 |
+
manifest: Manifest = Manifest.load(manifest_path)
|
| 860 |
+
|
| 861 |
+
# Split records if givens
|
| 862 |
+
if data_config.split is not None:
|
| 863 |
+
with Path(data_config.split).open("r") as f:
|
| 864 |
+
split = {x.lower() for x in f.read().splitlines()}
|
| 865 |
+
|
| 866 |
+
train_records = []
|
| 867 |
+
val_records = []
|
| 868 |
+
for record in manifest.records:
|
| 869 |
+
if record.id.lower() in split:
|
| 870 |
+
val_records.append(record)
|
| 871 |
+
else:
|
| 872 |
+
train_records.append(record)
|
| 873 |
+
else:
|
| 874 |
+
train_records = manifest.records
|
| 875 |
+
if cfg.overfit is None:
|
| 876 |
+
val_records = []
|
| 877 |
+
else:
|
| 878 |
+
print("Warning: modified overfit val behavior.")
|
| 879 |
+
val_records = manifest.records[: cfg.overfit]
|
| 880 |
+
|
| 881 |
+
print("train_records before filter", len(train_records))
|
| 882 |
+
|
| 883 |
+
# Apply dataset-specific filters
|
| 884 |
+
if data_config.filters is not None:
|
| 885 |
+
train_records = [
|
| 886 |
+
record
|
| 887 |
+
for record in train_records
|
| 888 |
+
if all(f.filter(record) for f in data_config.filters)
|
| 889 |
+
]
|
| 890 |
+
|
| 891 |
+
# Train with subset of data
|
| 892 |
+
if data_config.use_train_subset is not None:
|
| 893 |
+
# Shuffle train_records list
|
| 894 |
+
assert 0 < data_config.use_train_subset < 1.0
|
| 895 |
+
rng = np.random.default_rng(cfg.random_seed)
|
| 896 |
+
rng.shuffle(train_records)
|
| 897 |
+
train_records = train_records[
|
| 898 |
+
0 : int(len(train_records) * data_config.use_train_subset)
|
| 899 |
+
]
|
| 900 |
+
print("train_records after filter", len(train_records))
|
| 901 |
+
print("val_records after filter", len(val_records))
|
| 902 |
+
|
| 903 |
+
# Get samples
|
| 904 |
+
train_samples: list[Sample] = data_config.sampler.sample(train_records)
|
| 905 |
+
val_samples: list[Sample] = [Sample(r.id) for r in val_records]
|
| 906 |
+
|
| 907 |
+
# Convert samples to pandas dataframe to avoid copy-on-write behavior
|
| 908 |
+
train_samples = pd.DataFrame(
|
| 909 |
+
[
|
| 910 |
+
(
|
| 911 |
+
r.record_id,
|
| 912 |
+
r.chain_id,
|
| 913 |
+
r.interface_id,
|
| 914 |
+
r.weight,
|
| 915 |
+
)
|
| 916 |
+
for r in train_samples
|
| 917 |
+
],
|
| 918 |
+
columns=["record_id", "chain_id", "interface_id", "weight"],
|
| 919 |
+
)
|
| 920 |
+
val_samples = pd.DataFrame(
|
| 921 |
+
[s.record_id for s in val_samples], columns=["record_id"]
|
| 922 |
+
)
|
| 923 |
+
|
| 924 |
+
# Use appropriate string type
|
| 925 |
+
train_samples = train_samples.replace({np.nan: None})
|
| 926 |
+
val_samples = val_samples.replace({np.nan: None})
|
| 927 |
+
train_samples["record_id"] = train_samples["record_id"].astype("string")
|
| 928 |
+
val_samples["record_id"] = val_samples["record_id"].astype("string")
|
| 929 |
+
|
| 930 |
+
del manifest, train_records, val_records
|
| 931 |
+
# Create train dataset
|
| 932 |
+
if data_config.prob > 0:
|
| 933 |
+
train.append(
|
| 934 |
+
Dataset(
|
| 935 |
+
samples=train_samples,
|
| 936 |
+
record_dir=record_dir,
|
| 937 |
+
struct_dir=struct_dir,
|
| 938 |
+
msa_dir=msa_dir,
|
| 939 |
+
moldir=moldir,
|
| 940 |
+
prob=data_config.prob,
|
| 941 |
+
cropper=data_config.cropper,
|
| 942 |
+
tokenizer=cfg.tokenizer,
|
| 943 |
+
featurizer=cfg.featurizer,
|
| 944 |
+
val_group=data_config.val_group,
|
| 945 |
+
symmetry_correction=data_config.symmetry_correction,
|
| 946 |
+
override_bfactor=data_config.override_bfactor,
|
| 947 |
+
override_method=data_config.override_method,
|
| 948 |
+
selector=cfg.selector,
|
| 949 |
+
)
|
| 950 |
+
)
|
| 951 |
+
|
| 952 |
+
# Create validation dataset
|
| 953 |
+
if len(val_samples) > 0:
|
| 954 |
+
val.append(
|
| 955 |
+
Dataset(
|
| 956 |
+
samples=val_samples,
|
| 957 |
+
record_dir=record_dir,
|
| 958 |
+
struct_dir=struct_dir,
|
| 959 |
+
msa_dir=msa_dir,
|
| 960 |
+
moldir=moldir,
|
| 961 |
+
prob=data_config.prob,
|
| 962 |
+
cropper=data_config.cropper,
|
| 963 |
+
tokenizer=cfg.tokenizer,
|
| 964 |
+
featurizer=cfg.featurizer,
|
| 965 |
+
val_group=data_config.val_group,
|
| 966 |
+
symmetry_correction=data_config.symmetry_correction,
|
| 967 |
+
selector=cfg.selector,
|
| 968 |
+
)
|
| 969 |
+
)
|
| 970 |
+
|
| 971 |
+
# Print dataset sizes
|
| 972 |
+
for dataset in train:
|
| 973 |
+
dataset: Dataset
|
| 974 |
+
print(f"Training dataset size: {len(dataset.samples)}")
|
| 975 |
+
|
| 976 |
+
self.val_group_mapper = defaultdict(dict)
|
| 977 |
+
|
| 978 |
+
for i, dataset in enumerate(train if cfg.overfit is not None else val):
|
| 979 |
+
dataset: Dataset
|
| 980 |
+
print(f"Validation dataset size: {len(dataset.samples)}")
|
| 981 |
+
self.val_group_mapper[i]["label"] = dataset.val_group
|
| 982 |
+
self.val_group_mapper[i]["symmetry_correction"] = (
|
| 983 |
+
# If overfit, use symmetry_correction from val dataset instead of training dataset
|
| 984 |
+
dataset.symmetry_correction
|
| 985 |
+
if cfg.overfit is None
|
| 986 |
+
else data_config.symmetry_correction
|
| 987 |
+
)
|
| 988 |
+
|
| 989 |
+
# Load canonical molecules
|
| 990 |
+
canonicals = load_canonicals(cfg.moldir)
|
| 991 |
+
|
| 992 |
+
# Create wrapper datasets
|
| 993 |
+
self._train_set = TrainingDataset(
|
| 994 |
+
datasets=train,
|
| 995 |
+
canonicals=canonicals,
|
| 996 |
+
moldir=cfg.moldir,
|
| 997 |
+
samples_per_epoch=cfg.samples_per_epoch,
|
| 998 |
+
max_atoms=cfg.max_atoms,
|
| 999 |
+
max_tokens=cfg.max_tokens,
|
| 1000 |
+
max_seqs=cfg.max_seqs,
|
| 1001 |
+
pad_to_max_atoms=cfg.pad_to_max_atoms,
|
| 1002 |
+
pad_to_max_tokens=cfg.pad_to_max_tokens,
|
| 1003 |
+
pad_to_max_seqs=cfg.pad_to_max_seqs,
|
| 1004 |
+
atoms_per_window_queries=cfg.atoms_per_window_queries,
|
| 1005 |
+
min_dist=cfg.min_dist,
|
| 1006 |
+
max_dist=cfg.max_dist,
|
| 1007 |
+
num_bins=cfg.num_bins,
|
| 1008 |
+
overfit=cfg.overfit,
|
| 1009 |
+
return_symmetries=cfg.return_train_symmetries,
|
| 1010 |
+
single_sequence_prop=cfg.single_sequence_prop_training,
|
| 1011 |
+
msa_sampling=cfg.msa_sampling_training,
|
| 1012 |
+
use_msa=cfg.use_msa,
|
| 1013 |
+
compute_frames=cfg.compute_frames,
|
| 1014 |
+
backbone_only=cfg.backbone_only,
|
| 1015 |
+
atom14=cfg.atom14,
|
| 1016 |
+
atom37=cfg.atom37,
|
| 1017 |
+
design=cfg.design,
|
| 1018 |
+
inverse_fold=cfg.inverse_fold,
|
| 1019 |
+
disulfide_prob=cfg.disulfide_prob,
|
| 1020 |
+
disulfide_on=cfg.disulfide_on,
|
| 1021 |
+
)
|
| 1022 |
+
self._val_set = ValidationDataset(
|
| 1023 |
+
datasets=train if cfg.overfit is not None else val,
|
| 1024 |
+
canonicals=canonicals,
|
| 1025 |
+
moldir=cfg.moldir,
|
| 1026 |
+
seed=cfg.random_seed,
|
| 1027 |
+
max_atoms=cfg.max_atoms,
|
| 1028 |
+
max_tokens=cfg.max_tokens,
|
| 1029 |
+
max_seqs=cfg.max_seqs,
|
| 1030 |
+
pad_to_max_atoms=cfg.pad_to_max_atoms,
|
| 1031 |
+
pad_to_max_tokens=cfg.pad_to_max_tokens,
|
| 1032 |
+
pad_to_max_seqs=cfg.pad_to_max_seqs,
|
| 1033 |
+
atoms_per_window_queries=cfg.atoms_per_window_queries,
|
| 1034 |
+
min_dist=cfg.min_dist,
|
| 1035 |
+
max_dist=cfg.max_dist,
|
| 1036 |
+
num_bins=cfg.num_bins,
|
| 1037 |
+
overfit=cfg.overfit,
|
| 1038 |
+
return_symmetries=cfg.return_val_symmetries,
|
| 1039 |
+
compute_frames=cfg.compute_frames,
|
| 1040 |
+
backbone_only=cfg.backbone_only,
|
| 1041 |
+
atom14=cfg.atom14,
|
| 1042 |
+
atom37=cfg.atom37,
|
| 1043 |
+
design=cfg.design,
|
| 1044 |
+
inverse_fold=cfg.inverse_fold,
|
| 1045 |
+
disulfide_prob=cfg.disulfide_prob,
|
| 1046 |
+
disulfide_on=cfg.disulfide_on,
|
| 1047 |
+
)
|
| 1048 |
+
|
| 1049 |
+
self.monomer_split = cfg.monomer_split
|
| 1050 |
+
print("monomer_split", self.monomer_split)
|
| 1051 |
+
if self.monomer_split is not None:
|
| 1052 |
+
with Path(self.monomer_split).open("r") as f:
|
| 1053 |
+
monomer_ids = [x.lower() for x in f.read().splitlines()]
|
| 1054 |
+
print("monomer_split", monomer_ids)
|
| 1055 |
+
|
| 1056 |
+
dataset = data_protein_binder.Dataset(
|
| 1057 |
+
struct_dir=Path(cfg.monomer_target_dir) / "structures",
|
| 1058 |
+
record_dir=Path(cfg.monomer_target_dir) / "records",
|
| 1059 |
+
target_ids=monomer_ids,
|
| 1060 |
+
seq_len=cfg.monomer_seq_len,
|
| 1061 |
+
tokenizer=cfg.tokenizer,
|
| 1062 |
+
featurizer=cfg.featurizer,
|
| 1063 |
+
)
|
| 1064 |
+
|
| 1065 |
+
# Load canonical molecules
|
| 1066 |
+
canonicals = load_canonicals(cfg.moldir)
|
| 1067 |
+
|
| 1068 |
+
self.monomer_val_set = data_protein_binder.PredictionDataset(
|
| 1069 |
+
dataset=dataset,
|
| 1070 |
+
canonicals=canonicals,
|
| 1071 |
+
moldir=Path(cfg.moldir),
|
| 1072 |
+
backbone_only=cfg.backbone_only,
|
| 1073 |
+
atom14=cfg.atom14,
|
| 1074 |
+
atom37=cfg.atom37,
|
| 1075 |
+
design=cfg.design,
|
| 1076 |
+
target_structure_condition=cfg.monomer_target_structure_condition,
|
| 1077 |
+
inverse_fold=cfg.inverse_fold,
|
| 1078 |
+
disulfide_prob=cfg.disulfide_prob,
|
| 1079 |
+
disulfide_on=cfg.disulfide_on,
|
| 1080 |
+
)
|
| 1081 |
+
|
| 1082 |
+
self.ligand_split = cfg.ligand_split
|
| 1083 |
+
print("ligand_split", self.ligand_split)
|
| 1084 |
+
if self.ligand_split is not None:
|
| 1085 |
+
with Path(self.ligand_split).open("r") as f:
|
| 1086 |
+
ligand_ids = [x.lower() for x in f.read().splitlines()]
|
| 1087 |
+
print("ligand_split", ligand_ids)
|
| 1088 |
+
|
| 1089 |
+
dataset = data_ligands.Dataset(
|
| 1090 |
+
struct_dir=Path(cfg.ligand_target_dir) / "structures",
|
| 1091 |
+
record_dir=Path(cfg.ligand_target_dir) / "records",
|
| 1092 |
+
target_ids=ligand_ids,
|
| 1093 |
+
min_len=cfg.ligand_seq_len,
|
| 1094 |
+
max_len=cfg.ligand_seq_len,
|
| 1095 |
+
tokenizer=cfg.tokenizer,
|
| 1096 |
+
featurizer=cfg.featurizer,
|
| 1097 |
+
)
|
| 1098 |
+
|
| 1099 |
+
# Load canonical molecules
|
| 1100 |
+
canonicals = load_canonicals(cfg.moldir)
|
| 1101 |
+
|
| 1102 |
+
self.ligand_val_set = data_ligands.PredictionDataset(
|
| 1103 |
+
dataset=dataset,
|
| 1104 |
+
canonicals=canonicals,
|
| 1105 |
+
moldir=Path(cfg.moldir),
|
| 1106 |
+
backbone_only=cfg.backbone_only,
|
| 1107 |
+
atom14=cfg.atom14,
|
| 1108 |
+
atom37=cfg.atom37,
|
| 1109 |
+
design=cfg.design,
|
| 1110 |
+
disulfide_prob=cfg.disulfide_prob,
|
| 1111 |
+
disulfide_on=cfg.disulfide_on,
|
| 1112 |
+
)
|
| 1113 |
+
|
| 1114 |
+
def setup(self, stage: Optional[str] = None) -> None: # noqa: ARG002 (unused)
|
| 1115 |
+
"""Run the setup for the DataModule.
|
| 1116 |
+
|
| 1117 |
+
Parameters
|
| 1118 |
+
----------
|
| 1119 |
+
stage : str, optional
|
| 1120 |
+
The stage, one of 'fit', 'validate', 'test'.
|
| 1121 |
+
|
| 1122 |
+
"""
|
| 1123 |
+
return
|
| 1124 |
+
|
| 1125 |
+
def train_dataloader(self) -> DataLoader:
|
| 1126 |
+
"""Get the training dataloader.
|
| 1127 |
+
|
| 1128 |
+
Returns
|
| 1129 |
+
-------
|
| 1130 |
+
DataLoader
|
| 1131 |
+
The training dataloader.
|
| 1132 |
+
|
| 1133 |
+
"""
|
| 1134 |
+
return DataLoader(
|
| 1135 |
+
self._train_set,
|
| 1136 |
+
batch_size=self.cfg.batch_size,
|
| 1137 |
+
num_workers=self.cfg.num_workers,
|
| 1138 |
+
pin_memory=self.cfg.pin_memory,
|
| 1139 |
+
shuffle=False,
|
| 1140 |
+
collate_fn=collate,
|
| 1141 |
+
)
|
| 1142 |
+
|
| 1143 |
+
def val_dataloader(self) -> DataLoader:
|
| 1144 |
+
"""Get the validation dataloader.
|
| 1145 |
+
|
| 1146 |
+
Returns
|
| 1147 |
+
-------
|
| 1148 |
+
DataLoader
|
| 1149 |
+
The validation dataloader.s
|
| 1150 |
+
|
| 1151 |
+
"""
|
| 1152 |
+
val_loaders = []
|
| 1153 |
+
val_loaders.append(
|
| 1154 |
+
DataLoader(
|
| 1155 |
+
self._val_set,
|
| 1156 |
+
batch_size=self.cfg.val_batch_size,
|
| 1157 |
+
num_workers=self.cfg.num_workers if not self.inverse_fold else 1,
|
| 1158 |
+
pin_memory=self.cfg.num_workers if not self.inverse_fold else False,
|
| 1159 |
+
shuffle=False,
|
| 1160 |
+
collate_fn=collate,
|
| 1161 |
+
)
|
| 1162 |
+
)
|
| 1163 |
+
if self.monomer_split is not None:
|
| 1164 |
+
val_loaders.append(
|
| 1165 |
+
DataLoader(
|
| 1166 |
+
self.monomer_val_set,
|
| 1167 |
+
batch_size=self.cfg.val_batch_size,
|
| 1168 |
+
num_workers=self.cfg.num_workers if not self.inverse_fold else 1,
|
| 1169 |
+
pin_memory=self.cfg.pin_memory if not self.inverse_fold else False,
|
| 1170 |
+
shuffle=False,
|
| 1171 |
+
collate_fn=data_protein_binder.collate,
|
| 1172 |
+
)
|
| 1173 |
+
)
|
| 1174 |
+
if self.ligand_split is not None:
|
| 1175 |
+
val_loaders.append(
|
| 1176 |
+
DataLoader(
|
| 1177 |
+
self.ligand_val_set,
|
| 1178 |
+
batch_size=self.cfg.val_batch_size,
|
| 1179 |
+
num_workers=self.cfg.num_workers if not self.inverse_fold else 1,
|
| 1180 |
+
pin_memory=self.cfg.pin_memory if not self.inverse_fold else False,
|
| 1181 |
+
shuffle=False,
|
| 1182 |
+
collate_fn=data_ligands.collate,
|
| 1183 |
+
)
|
| 1184 |
+
)
|
| 1185 |
+
return val_loaders
|
| 1186 |
+
|
| 1187 |
+
def predict_dataloader(self) -> DataLoader:
|
| 1188 |
+
return DataLoader(
|
| 1189 |
+
self._val_set,
|
| 1190 |
+
batch_size=self.cfg.val_batch_size,
|
| 1191 |
+
num_workers=self.cfg.num_workers,
|
| 1192 |
+
pin_memory=self.cfg.pin_memory,
|
| 1193 |
+
shuffle=False,
|
| 1194 |
+
collate_fn=collate,
|
| 1195 |
+
)
|
| 1196 |
+
|
| 1197 |
+
def transfer_batch_to_device(
|
| 1198 |
+
self,
|
| 1199 |
+
batch: Dict,
|
| 1200 |
+
device: torch.device,
|
| 1201 |
+
dataloader_idx: int, # noqa: ARG002
|
| 1202 |
+
) -> Dict:
|
| 1203 |
+
"""Transfer a batch to the given device.
|
| 1204 |
+
|
| 1205 |
+
Parameters
|
| 1206 |
+
----------
|
| 1207 |
+
batch : Dict
|
| 1208 |
+
The batch to transfer.
|
| 1209 |
+
device : torch.device
|
| 1210 |
+
The device to transfer to.
|
| 1211 |
+
dataloader_idx : int
|
| 1212 |
+
The dataloader index.
|
| 1213 |
+
|
| 1214 |
+
Returns
|
| 1215 |
+
-------
|
| 1216 |
+
np.Any
|
| 1217 |
+
The transferred batch.
|
| 1218 |
+
|
| 1219 |
+
"""
|
| 1220 |
+
for key in batch:
|
| 1221 |
+
if key not in [
|
| 1222 |
+
"all_coords",
|
| 1223 |
+
"all_resolved_mask",
|
| 1224 |
+
"crop_to_all_atom_map",
|
| 1225 |
+
"chain_symmetries",
|
| 1226 |
+
"chain_swaps",
|
| 1227 |
+
"amino_acids_symmetries",
|
| 1228 |
+
"ligand_symmetries",
|
| 1229 |
+
"activity_name",
|
| 1230 |
+
"activity_qualifier",
|
| 1231 |
+
"sid",
|
| 1232 |
+
"cid",
|
| 1233 |
+
"normalized_protein_accession",
|
| 1234 |
+
"pair_id",
|
| 1235 |
+
"ligand_edge_index",
|
| 1236 |
+
"ligand_edge_lower_bounds",
|
| 1237 |
+
"ligand_edge_upper_bounds",
|
| 1238 |
+
"ligand_edge_bond_mask",
|
| 1239 |
+
"ligand_edge_angle_mask",
|
| 1240 |
+
"connections_edge_index",
|
| 1241 |
+
"ligand_chiral_atom_index",
|
| 1242 |
+
"ligand_chiral_check_mask",
|
| 1243 |
+
"ligand_chiral_atom_orientations",
|
| 1244 |
+
"ligand_stereo_bond_index",
|
| 1245 |
+
"ligand_stereo_check_mask",
|
| 1246 |
+
"ligand_stereo_bond_orientations",
|
| 1247 |
+
"ligand_aromatic_5_ring_index",
|
| 1248 |
+
"ligand_aromatic_6_ring_index",
|
| 1249 |
+
"ligand_planar_double_bond_index",
|
| 1250 |
+
"pdb_id",
|
| 1251 |
+
"id",
|
| 1252 |
+
"tokenized",
|
| 1253 |
+
"structure",
|
| 1254 |
+
"structure_bonds",
|
| 1255 |
+
"extra_mols",
|
| 1256 |
+
]:
|
| 1257 |
+
if hasattr(batch[key], "to"):
|
| 1258 |
+
batch[key] = batch[key].to(device)
|
| 1259 |
+
return batch
|
model/boltzgen/task/train/train.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import os
|
| 3 |
+
import warnings
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import time
|
| 6 |
+
from typing import Optional
|
| 7 |
+
import pytorch_lightning as pl
|
| 8 |
+
import torch
|
| 9 |
+
from omegaconf import OmegaConf, listconfig
|
| 10 |
+
from pytorch_lightning import LightningModule
|
| 11 |
+
from pytorch_lightning.callbacks.model_checkpoint import ModelCheckpoint
|
| 12 |
+
from pytorch_lightning.loggers import WandbLogger
|
| 13 |
+
from pytorch_lightning.strategies import DDPStrategy
|
| 14 |
+
|
| 15 |
+
from boltzgen.task.task import Task
|
| 16 |
+
from boltzgen.task.train.data import DataConfig, TrainingDataModule
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Training(Task):
|
| 20 |
+
"""Training configuration."""
|
| 21 |
+
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
data: DataConfig,
|
| 25 |
+
model: LightningModule,
|
| 26 |
+
output: str,
|
| 27 |
+
name: str,
|
| 28 |
+
trainer: Optional[dict] = None,
|
| 29 |
+
resume: Optional[str] = None,
|
| 30 |
+
pretrained: Optional[str] = None,
|
| 31 |
+
wandb: Optional[dict] = None,
|
| 32 |
+
disable_checkpoint: bool = False,
|
| 33 |
+
slurm: bool = False,
|
| 34 |
+
matmul_precision: Optional[str] = None,
|
| 35 |
+
find_unused_parameters: Optional[bool] = False,
|
| 36 |
+
ddp_timeout_seconds: Optional[int] = 6400,
|
| 37 |
+
save_every_n_train_steps: Optional[int] = None,
|
| 38 |
+
save_top_k: Optional[int] = 1,
|
| 39 |
+
validation_only: bool = False,
|
| 40 |
+
debug: bool = False,
|
| 41 |
+
strict_loading: bool = True,
|
| 42 |
+
metric_mode: str = "max",
|
| 43 |
+
) -> None:
|
| 44 |
+
"""Initialize training configuration.
|
| 45 |
+
|
| 46 |
+
Parameters
|
| 47 |
+
----------
|
| 48 |
+
data : DataConfig
|
| 49 |
+
The data configuration.
|
| 50 |
+
model : LightningModule
|
| 51 |
+
The model.
|
| 52 |
+
output : str
|
| 53 |
+
The output directory.
|
| 54 |
+
name : str
|
| 55 |
+
The output subdirectory and wandb run name.
|
| 56 |
+
trainer : Optional[dict], optional
|
| 57 |
+
The trainer configuration, by default None.
|
| 58 |
+
resume : Optional[str], optional
|
| 59 |
+
The resume checkpoint, by default None
|
| 60 |
+
pretrained : Optional[str], optional
|
| 61 |
+
The pretrained model, by default None
|
| 62 |
+
wandb : Optional[dict], optional
|
| 63 |
+
The wandb configuration, by default None
|
| 64 |
+
disable_checkpoint : bool, optional
|
| 65 |
+
Disable checkpoint, by default False
|
| 66 |
+
|
| 67 |
+
"""
|
| 68 |
+
if not isinstance(data, DataConfig):
|
| 69 |
+
data = DataConfig(**data)
|
| 70 |
+
|
| 71 |
+
self.data = data
|
| 72 |
+
self.model = model
|
| 73 |
+
self.output = output
|
| 74 |
+
self.name = name
|
| 75 |
+
self.trainer = trainer
|
| 76 |
+
self.resume = resume
|
| 77 |
+
self.pretrained = pretrained
|
| 78 |
+
self.wandb = wandb
|
| 79 |
+
self.disable_checkpoint = disable_checkpoint
|
| 80 |
+
self.slurm = slurm
|
| 81 |
+
self.matmul_precision = matmul_precision
|
| 82 |
+
self.find_unused_parameters = find_unused_parameters
|
| 83 |
+
self.ddp_timeout_seconds = ddp_timeout_seconds
|
| 84 |
+
self.save_every_n_train_steps = save_every_n_train_steps
|
| 85 |
+
self.save_top_k = save_top_k
|
| 86 |
+
self.validation_only = validation_only
|
| 87 |
+
self.debug = debug
|
| 88 |
+
self.strict_loading = strict_loading
|
| 89 |
+
self.metric_mode = metric_mode
|
| 90 |
+
|
| 91 |
+
def run(self, config: OmegaConf) -> None:
|
| 92 |
+
"""Run training.
|
| 93 |
+
|
| 94 |
+
Parameters
|
| 95 |
+
----------
|
| 96 |
+
config : OmegaConf
|
| 97 |
+
The configuration for the task, for bookkeeping.
|
| 98 |
+
|
| 99 |
+
"""
|
| 100 |
+
# Disable some warnings
|
| 101 |
+
warnings.filterwarnings(
|
| 102 |
+
"ignore", ".*when logging on epoch level in distributed setting.*"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Experiment with this during training (high or medium)
|
| 106 |
+
if self.matmul_precision is not None:
|
| 107 |
+
torch.set_float32_matmul_precision(self.matmul_precision)
|
| 108 |
+
|
| 109 |
+
# Create trainer dict
|
| 110 |
+
if self.trainer is None:
|
| 111 |
+
self.trainer = {}
|
| 112 |
+
|
| 113 |
+
# Flip some arguments in debug mode
|
| 114 |
+
devices = self.trainer.get("devices", 1)
|
| 115 |
+
|
| 116 |
+
if self.debug:
|
| 117 |
+
self.slurm = False
|
| 118 |
+
if isinstance(devices, int):
|
| 119 |
+
devices = 1
|
| 120 |
+
elif isinstance(devices, (list, listconfig.ListConfig)):
|
| 121 |
+
devices = [devices[0]]
|
| 122 |
+
self.trainer["devices"] = devices
|
| 123 |
+
self.data.num_workers = 0
|
| 124 |
+
if self.wandb:
|
| 125 |
+
self.wandb = None
|
| 126 |
+
|
| 127 |
+
# slurm
|
| 128 |
+
if self.slurm:
|
| 129 |
+
self.trainer["devices"] = int(
|
| 130 |
+
os.environ.get("SLURM_NTASKS_PER_NODE", "auto")
|
| 131 |
+
)
|
| 132 |
+
self.trainer["num_nodes"] = int(os.environ.get("SLURM_NNODES", 1))
|
| 133 |
+
|
| 134 |
+
# Create objects
|
| 135 |
+
data_module = TrainingDataModule(self.data)
|
| 136 |
+
model_module = self.model
|
| 137 |
+
|
| 138 |
+
if self.pretrained and not self.resume:
|
| 139 |
+
file_path = self.pretrained
|
| 140 |
+
|
| 141 |
+
print(f"Loading model from {file_path}")
|
| 142 |
+
model_module = type(model_module).load_from_checkpoint(
|
| 143 |
+
file_path, map_location="cpu", strict=False, weights_only=False, **(model_module.hparams)
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# Create checkpoint callback
|
| 147 |
+
callbacks = self.trainer.get("callbacks", [])
|
| 148 |
+
dirpath = f"{self.output}/{self.name}"
|
| 149 |
+
os.makedirs(dirpath, exist_ok=True)
|
| 150 |
+
|
| 151 |
+
if not self.disable_checkpoint:
|
| 152 |
+
if self.slurm:
|
| 153 |
+
jobid = os.environ.get("SLURM_JOB_ID", "")
|
| 154 |
+
dirpath = Path(self.output) / jobid
|
| 155 |
+
dirpath.mkdir(parents=True, exist_ok=True)
|
| 156 |
+
print(
|
| 157 |
+
"Configuring ModelCheckpoint for SLURM to directory: ", str(dirpath)
|
| 158 |
+
)
|
| 159 |
+
mc = ModelCheckpoint(
|
| 160 |
+
monitor="val/lddt",
|
| 161 |
+
save_top_k=self.save_top_k,
|
| 162 |
+
save_last=True,
|
| 163 |
+
mode=self.metric_mode,
|
| 164 |
+
every_n_epochs=1,
|
| 165 |
+
)
|
| 166 |
+
callbacks.append(mc)
|
| 167 |
+
|
| 168 |
+
# Create wandb logger
|
| 169 |
+
loggers = []
|
| 170 |
+
if self.wandb:
|
| 171 |
+
extra = {}
|
| 172 |
+
if self.slurm:
|
| 173 |
+
ckpt_path = (
|
| 174 |
+
"hpc"
|
| 175 |
+
if len([f for f in os.listdir(dirpath) if f.startswith("hpc")])
|
| 176 |
+
else None
|
| 177 |
+
)
|
| 178 |
+
extra = {"resume": "must"} if ckpt_path else {}
|
| 179 |
+
|
| 180 |
+
wdb_logger = WandbLogger(
|
| 181 |
+
name=self.name,
|
| 182 |
+
group=self.wandb["group"],
|
| 183 |
+
save_dir=dirpath,
|
| 184 |
+
dir=f".tmp_wandb/{time.time()}",
|
| 185 |
+
project=self.wandb["project"],
|
| 186 |
+
entity=self.wandb["entity"],
|
| 187 |
+
log_model=False,
|
| 188 |
+
id=os.environ.get("SLURM_JOB_ID", None) if self.slurm else None,
|
| 189 |
+
**extra,
|
| 190 |
+
)
|
| 191 |
+
loggers.append(wdb_logger)
|
| 192 |
+
# Save the config to wandb
|
| 193 |
+
# This fails when not on rank0, so just catching
|
| 194 |
+
try:
|
| 195 |
+
config_out = Path(wdb_logger.experiment.dir) / "run.yaml"
|
| 196 |
+
with Path.open(config_out, "w") as f:
|
| 197 |
+
OmegaConf.save(config, f)
|
| 198 |
+
wdb_logger.experiment.save(str(config_out))
|
| 199 |
+
except Exception: # noqa: BLE001, S110
|
| 200 |
+
pass
|
| 201 |
+
|
| 202 |
+
# Set up trainer
|
| 203 |
+
strategy = "auto"
|
| 204 |
+
if (isinstance(devices, int) and devices > 1) or (
|
| 205 |
+
isinstance(devices, (list, listconfig.ListConfig)) and len(devices) > 1
|
| 206 |
+
):
|
| 207 |
+
strategy = DDPStrategy(
|
| 208 |
+
find_unused_parameters=self.find_unused_parameters,
|
| 209 |
+
timeout=datetime.timedelta(seconds=self.ddp_timeout_seconds),
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
trainer = pl.Trainer(
|
| 213 |
+
default_root_dir=str(dirpath),
|
| 214 |
+
strategy=strategy,
|
| 215 |
+
callbacks=callbacks,
|
| 216 |
+
logger=loggers,
|
| 217 |
+
enable_checkpointing=not self.disable_checkpoint,
|
| 218 |
+
reload_dataloaders_every_n_epochs=1,
|
| 219 |
+
**self.trainer,
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
# Run training
|
| 223 |
+
if self.slurm:
|
| 224 |
+
# If we are on SLURM, we need to check if we have a checkpoint for this job
|
| 225 |
+
ckpt_path = (
|
| 226 |
+
"hpc"
|
| 227 |
+
if len([f for f in os.listdir(dirpath) if f.startswith("hpc")])
|
| 228 |
+
else None
|
| 229 |
+
)
|
| 230 |
+
# If none check with we have a resume checkpoint passed in
|
| 231 |
+
if ckpt_path is None:
|
| 232 |
+
ckpt_path = self.resume
|
| 233 |
+
else:
|
| 234 |
+
ckpt_path = self.resume
|
| 235 |
+
|
| 236 |
+
if not self.strict_loading:
|
| 237 |
+
model_module.strict_loading = False
|
| 238 |
+
|
| 239 |
+
if self.validation_only:
|
| 240 |
+
trainer.validate(
|
| 241 |
+
model_module,
|
| 242 |
+
datamodule=data_module,
|
| 243 |
+
ckpt_path=ckpt_path,
|
| 244 |
+
)
|
| 245 |
+
else:
|
| 246 |
+
trainer.fit(
|
| 247 |
+
model_module,
|
| 248 |
+
datamodule=data_module,
|
| 249 |
+
ckpt_path=ckpt_path,
|
| 250 |
+
)
|
model/boltzgen/utils/__init__.py
ADDED
|
File without changes
|
model/boltzgen/utils/pipeline_progress_bar.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Custom PyTorch Lightning progress bar callback that displays pipeline step information.
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
from pytorch_lightning.callbacks import TQDMProgressBar
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class PipelineProgressBar(TQDMProgressBar):
|
| 11 |
+
"""Custom progress bar that shows the current pipeline step."""
|
| 12 |
+
|
| 13 |
+
def __init__(self, refresh_rate: int = 1, process_position: int = 0):
|
| 14 |
+
"""Initialize the pipeline progress bar.
|
| 15 |
+
|
| 16 |
+
Parameters
|
| 17 |
+
----------
|
| 18 |
+
refresh_rate : int
|
| 19 |
+
Refresh rate for the progress bar
|
| 20 |
+
process_position : int
|
| 21 |
+
Position of the process for multi-processing
|
| 22 |
+
"""
|
| 23 |
+
super().__init__(refresh_rate=refresh_rate, process_position=process_position)
|
| 24 |
+
self._original_tqdm = None
|
| 25 |
+
self._patch_tqdm()
|
| 26 |
+
|
| 27 |
+
def _get_pipeline_info(self) -> str:
|
| 28 |
+
"""Get pipeline step information from environment variables."""
|
| 29 |
+
pipeline_step = os.environ.get("BOLTZGEN_PIPELINE_STEP", "")
|
| 30 |
+
pipeline_progress = os.environ.get("BOLTZGEN_PIPELINE_PROGRESS", "")
|
| 31 |
+
|
| 32 |
+
if pipeline_step:
|
| 33 |
+
if pipeline_progress:
|
| 34 |
+
return f"[{pipeline_progress}] {pipeline_step}"
|
| 35 |
+
else:
|
| 36 |
+
return f"[Pipeline] {pipeline_step}"
|
| 37 |
+
return ""
|
| 38 |
+
|
| 39 |
+
def _update_bar_description(self, bar):
|
| 40 |
+
"""Helper method to update a progress bar description with pipeline info."""
|
| 41 |
+
if bar is not None:
|
| 42 |
+
pipeline_info = self._get_pipeline_info()
|
| 43 |
+
if pipeline_info:
|
| 44 |
+
# Update the description to include pipeline step info
|
| 45 |
+
current_desc = getattr(bar, 'desc', '') or ''
|
| 46 |
+
if current_desc:
|
| 47 |
+
new_desc = f"{pipeline_info} - {current_desc}"
|
| 48 |
+
bar.set_description(new_desc)
|
| 49 |
+
else:
|
| 50 |
+
bar.set_description(pipeline_info)
|
| 51 |
+
return bar
|
| 52 |
+
|
| 53 |
+
def init_predict_tqdm(self) -> None:
|
| 54 |
+
"""Initialize the prediction progress bar."""
|
| 55 |
+
bar = super().init_predict_tqdm()
|
| 56 |
+
return self._update_bar_description(bar)
|
| 57 |
+
|
| 58 |
+
def init_train_tqdm(self) -> None:
|
| 59 |
+
"""Initialize the training progress bar."""
|
| 60 |
+
bar = super().init_train_tqdm()
|
| 61 |
+
return self._update_bar_description(bar)
|
| 62 |
+
|
| 63 |
+
def init_validation_tqdm(self) -> None:
|
| 64 |
+
"""Initialize the validation progress bar."""
|
| 65 |
+
bar = super().init_validation_tqdm()
|
| 66 |
+
return self._update_bar_description(bar)
|
| 67 |
+
|
| 68 |
+
def init_test_tqdm(self) -> None:
|
| 69 |
+
"""Initialize the test progress bar."""
|
| 70 |
+
bar = super().init_test_tqdm()
|
| 71 |
+
return self._update_bar_description(bar)
|
| 72 |
+
|
| 73 |
+
def init_sanity_tqdm(self) -> None:
|
| 74 |
+
"""Initialize the sanity check progress bar."""
|
| 75 |
+
bar = super().init_sanity_tqdm()
|
| 76 |
+
return self._update_bar_description(bar)
|
| 77 |
+
|
| 78 |
+
def on_predict_start(self, trainer, pl_module):
|
| 79 |
+
"""Called when prediction starts - update all progress bars."""
|
| 80 |
+
super().on_predict_start(trainer, pl_module)
|
| 81 |
+
self._update_all_progress_bars()
|
| 82 |
+
|
| 83 |
+
def on_predict_batch_start(self, trainer, pl_module, batch, batch_idx):
|
| 84 |
+
"""Called before each prediction batch - update progress bars."""
|
| 85 |
+
super().on_predict_batch_start(trainer, pl_module, batch, batch_idx)
|
| 86 |
+
self._update_all_progress_bars()
|
| 87 |
+
|
| 88 |
+
def _update_all_progress_bars(self):
|
| 89 |
+
"""Update all existing progress bars with pipeline info."""
|
| 90 |
+
pipeline_info = self._get_pipeline_info()
|
| 91 |
+
if not pipeline_info:
|
| 92 |
+
return
|
| 93 |
+
|
| 94 |
+
# Try to find and update all progress bars
|
| 95 |
+
for attr_name in dir(self):
|
| 96 |
+
if ('progress' in attr_name.lower() or
|
| 97 |
+
'tqdm' in attr_name.lower() or
|
| 98 |
+
attr_name.endswith('_bar')):
|
| 99 |
+
try:
|
| 100 |
+
bar = getattr(self, attr_name)
|
| 101 |
+
if (bar is not None and
|
| 102 |
+
hasattr(bar, 'set_description') and
|
| 103 |
+
hasattr(bar, 'desc')):
|
| 104 |
+
current_desc = getattr(bar, 'desc', '') or ''
|
| 105 |
+
if pipeline_info not in current_desc:
|
| 106 |
+
if current_desc:
|
| 107 |
+
new_desc = f"{pipeline_info} - {current_desc}"
|
| 108 |
+
else:
|
| 109 |
+
new_desc = pipeline_info
|
| 110 |
+
bar.set_description(new_desc)
|
| 111 |
+
except (AttributeError, TypeError):
|
| 112 |
+
pass
|
| 113 |
+
|
| 114 |
+
def print(self, *args, **kwargs):
|
| 115 |
+
"""Override print method to intercept and modify all progress updates."""
|
| 116 |
+
# Check if this is updating a progress bar description and modify it
|
| 117 |
+
if args:
|
| 118 |
+
# Try to update any active progress bars before printing
|
| 119 |
+
self._update_all_progress_bars()
|
| 120 |
+
|
| 121 |
+
return super().print(*args, **kwargs)
|
| 122 |
+
|
| 123 |
+
def get_metrics(self, trainer, pl_module):
|
| 124 |
+
"""Override get_metrics to update progress bars on each metric update."""
|
| 125 |
+
metrics = super().get_metrics(trainer, pl_module)
|
| 126 |
+
self._update_all_progress_bars()
|
| 127 |
+
return metrics
|
| 128 |
+
|
| 129 |
+
def _patch_tqdm(self):
|
| 130 |
+
"""Patch tqdm to automatically add pipeline info to all progress bars."""
|
| 131 |
+
if self._original_tqdm is not None:
|
| 132 |
+
return # Already patched
|
| 133 |
+
|
| 134 |
+
# Store the original tqdm class
|
| 135 |
+
self._original_tqdm = tqdm
|
| 136 |
+
|
| 137 |
+
# Create a wrapper that adds pipeline info
|
| 138 |
+
def patched_tqdm(*args, **kwargs):
|
| 139 |
+
# Create the original tqdm instance
|
| 140 |
+
instance = self._original_tqdm(*args, **kwargs)
|
| 141 |
+
|
| 142 |
+
# Update its description with pipeline info
|
| 143 |
+
pipeline_info = self._get_pipeline_info()
|
| 144 |
+
if pipeline_info:
|
| 145 |
+
current_desc = kwargs.get('desc', '') or getattr(instance, 'desc', '') or ''
|
| 146 |
+
if pipeline_info not in current_desc:
|
| 147 |
+
if current_desc and current_desc.strip():
|
| 148 |
+
# Remove any trailing colons from current description to avoid double colons
|
| 149 |
+
current_desc = current_desc.rstrip(':').strip()
|
| 150 |
+
new_desc = f"{pipeline_info} - {current_desc}"
|
| 151 |
+
else:
|
| 152 |
+
new_desc = pipeline_info
|
| 153 |
+
instance.set_description(new_desc)
|
| 154 |
+
|
| 155 |
+
return instance
|
| 156 |
+
|
| 157 |
+
# Monkey patch tqdm globally during our callback's lifetime
|
| 158 |
+
import tqdm as tqdm_module
|
| 159 |
+
tqdm_module.tqdm = patched_tqdm
|
| 160 |
+
|
| 161 |
+
def _unpatch_tqdm(self):
|
| 162 |
+
"""Restore original tqdm."""
|
| 163 |
+
if self._original_tqdm is not None:
|
| 164 |
+
import tqdm as tqdm_module
|
| 165 |
+
tqdm_module.tqdm = self._original_tqdm
|
| 166 |
+
self._original_tqdm = None
|
| 167 |
+
|
| 168 |
+
def teardown(self, trainer, pl_module, stage):
|
| 169 |
+
"""Called when the callback is being torn down."""
|
| 170 |
+
super().teardown(trainer, pl_module, stage)
|
| 171 |
+
self._unpatch_tqdm()
|
model/boltzgen/utils/quiet.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging, warnings
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def quiet_startup() -> None:
|
| 5 |
+
warnings.filterwarnings("ignore", message=r".*predict_dataloader.*num_workers.*")
|
| 6 |
+
warnings.filterwarnings(
|
| 7 |
+
"ignore", message=r".*tensorboardX.*removed as a dependency.*"
|
| 8 |
+
)
|
| 9 |
+
warnings.filterwarnings(
|
| 10 |
+
"ignore",
|
| 11 |
+
message=r"The pynvml package is deprecated",
|
| 12 |
+
category=FutureWarning,
|
| 13 |
+
module=r"torch\.cuda",
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
logging.getLogger("pytorch_lightning").setLevel(logging.ERROR)
|
requirements.txt
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BoltzGen 0.3.2 on OneScience / Python 3.11 / DTK 25.04 / PyTorch 2.5.1
|
| 2 |
+
# Install only after OneScience is installed:
|
| 3 |
+
# python -m pip install --no-deps -r requirements.txt
|
| 4 |
+
|
| 5 |
+
# Additional dependencies absent from the OneScience base inventory.
|
| 6 |
+
hydride
|
| 7 |
+
pydssp
|
| 8 |
+
logomaker
|
| 9 |
+
frozendict
|
| 10 |
+
|
| 11 |
+
# Compatibility upgrade validated on SCNet. Lightning 1.8.6 rejects the
|
| 12 |
+
# precision=bf16-mixed value used by BoltzGen. Install with --no-deps so this
|
| 13 |
+
# upgrade does not replace the OneScience/DTK PyTorch build.
|
| 14 |
+
pytorch-lightning==2.5.6
|
| 15 |
+
|
| 16 |
+
# Supplied by OneScience; retained as the complete dependency inventory.
|
| 17 |
+
# Do not reinstall these packages over OneScience/DTK builds.
|
| 18 |
+
# torch==2.5.1 # DTK/DCU build supplied by OneScience
|
| 19 |
+
# numpy==1.26.3 # validated despite upstream pinning 2.0.2
|
| 20 |
+
# numba
|
| 21 |
+
# matplotlib
|
| 22 |
+
# biotite==1.0.1
|
| 23 |
+
# gemmi
|
| 24 |
+
# mashumaro==3.16
|
| 25 |
+
# einx==0.3.0
|
| 26 |
+
# einops>=0.7.0
|
| 27 |
+
# scikit-learn>=1.2.2,<=1.6.0
|
| 28 |
+
# hydra-core>=1.2.0
|
| 29 |
+
# omegaconf>=2.3.0
|
| 30 |
+
# rdkit>=2025.3.5
|
| 31 |
+
# pandas>=2.2.2
|
| 32 |
+
# biopython==1.84
|
| 33 |
+
# huggingface_hub
|
| 34 |
+
# pyyaml>=6.0
|
| 35 |
+
# scipy==1.14.1
|
| 36 |
+
# tqdm>=4.60.0
|
| 37 |
+
# torchmetrics
|
| 38 |
+
# pytorch-lightning==2.0.6 # OneScience inventory; upgraded above for BoltzGen
|
| 39 |
+
# wandb>=0.13.7 # training logging; disabled in smoke tests
|
| 40 |
+
# redis==7.0.0
|
| 41 |
+
# nvidia-ml-py>=12.535.133 # NVIDIA monitoring only; not needed on DCU
|
| 42 |
+
|
| 43 |
+
# Declared upstream but not imported by the validated paths.
|
| 44 |
+
# edit-distance
|
| 45 |
+
# pdbeccdutils
|
| 46 |
+
|
| 47 |
+
# Optional: antibody/nanobody CDR logo generation during filtering only.
|
| 48 |
+
# abnumber
|
| 49 |
+
|
| 50 |
+
# NVIDIA CUDA 12-only extensions: intentionally disabled on DCU.
|
| 51 |
+
# cuequivariance_ops_cu12>=0.5.0
|
| 52 |
+
# cuequivariance_ops_torch_cu12>=0.5.0
|
| 53 |
+
# cuequivariance_torch>=0.5.0
|
scripts/boltzgen.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run the bundled BoltzGen CLI without installing the source package."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
MODEL_DIR = ROOT / "model"
|
| 13 |
+
sys.path.insert(0, str(MODEL_DIR))
|
| 14 |
+
os.environ["PYTHONPATH"] = os.pathsep.join(
|
| 15 |
+
[str(MODEL_DIR), os.environ.get("PYTHONPATH", "")]
|
| 16 |
+
).rstrip(os.pathsep)
|
| 17 |
+
|
| 18 |
+
from boltzgen.cli.boltzgen import main # noqa: E402
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
main()
|
scripts/check_environment.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Report core OneScience/DTK and BoltzGen dependency versions."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import importlib
|
| 7 |
+
import sys
|
| 8 |
+
from importlib.metadata import PackageNotFoundError, version
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
sys.path.insert(0, str(ROOT / "model"))
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def package_version(distribution: str) -> str:
|
| 17 |
+
try:
|
| 18 |
+
return version(distribution)
|
| 19 |
+
except PackageNotFoundError:
|
| 20 |
+
return "NOT INSTALLED"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main() -> int:
|
| 24 |
+
print("python:", sys.version.split()[0])
|
| 25 |
+
try:
|
| 26 |
+
import torch
|
| 27 |
+
|
| 28 |
+
print("torch:", torch.__version__)
|
| 29 |
+
print("HIP:", torch.version.hip)
|
| 30 |
+
print("DCU available:", torch.cuda.is_available())
|
| 31 |
+
print("device count:", torch.cuda.device_count())
|
| 32 |
+
if torch.cuda.is_available() and torch.cuda.device_count():
|
| 33 |
+
print("device 0:", torch.cuda.get_device_name(0))
|
| 34 |
+
except Exception as exc: # noqa: BLE001
|
| 35 |
+
print("torch check failed:", repr(exc))
|
| 36 |
+
return 1
|
| 37 |
+
|
| 38 |
+
for distribution in (
|
| 39 |
+
"pytorch-lightning", "torchmetrics", "hydra-core", "omegaconf",
|
| 40 |
+
"biotite", "gemmi", "mashumaro", "einx", "hydride", "pydssp",
|
| 41 |
+
"logomaker", "frozendict",
|
| 42 |
+
):
|
| 43 |
+
print(f"{distribution}:", package_version(distribution))
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
importlib.import_module("boltzgen.cli.boltzgen")
|
| 47 |
+
print("boltzgen import: OK")
|
| 48 |
+
except Exception as exc: # noqa: BLE001
|
| 49 |
+
print("boltzgen import failed:", repr(exc))
|
| 50 |
+
return 1
|
| 51 |
+
return 0
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
raise SystemExit(main())
|
scripts/filter.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
scripts/slurm-example/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
slurm-example
|
| 2 |
+
=============
|
| 3 |
+
|
| 4 |
+
This directory contains an example for running BoltzGen on SLURM.
|
| 5 |
+
|
| 6 |
+
The approach we use is to launch a job array that runs many single-GPU jobs. Each job calls "boltzgen run" to go through all pipeline steps (design generation, refolding, etc.) for a small number of designs. After all the jobs have completed, on a login node we use "boltzgen merge" to merge all individual task results together, then "boltzgen run --steps filtering" to apply
|
| 7 |
+
filters to the combined set.
|
| 8 |
+
|
| 9 |
+
Note that other parallelization strategies could also work here. In particular, the individual BoltzGen pipeline steps (e.g. design generation, refolding) support
|
| 10 |
+
parallelization across multiple GPUs within the step.
|
| 11 |
+
|
| 12 |
+
You will need to modify [run.sh](run.sh) and probably also [run_job_array.slurm](run_job_array.slurm) for your site and analysis task.
|
| 13 |
+
|
| 14 |
+
After you've made your modifications, you can run:
|
| 15 |
+
|
| 16 |
+
```
|
| 17 |
+
$ bash run.sh submit
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
To submit the job array.
|
| 21 |
+
|
| 22 |
+
After that has finished, you can run:
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
$ bash run.sh process
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
to merge and filter the results.
|
scripts/slurm-example/run.sh
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
set -x
|
| 4 |
+
|
| 5 |
+
DESIGN_SPEC=/path/to/design/spec.yaml
|
| 6 |
+
MERGED_OUT=/path/to/your/output/directory
|
| 7 |
+
NUM_TASKS=20
|
| 8 |
+
NUM_DESIGNS_PER_TASK=1000
|
| 9 |
+
CONDA_ENVIRONMENT=/path/to/conda/environment/with/boltzgen
|
| 10 |
+
CACHE_DIR=/path/where/models/are/saved
|
| 11 |
+
ACCOUNT=FILL_THIS_IN
|
| 12 |
+
TIME=05:00:00
|
| 13 |
+
|
| 14 |
+
OUT="${MERGED_OUT}/task-outputs"
|
| 15 |
+
LOGS="${MERGED_OUT}/task-logs"
|
| 16 |
+
|
| 17 |
+
if [[ $# -ne 1 ]]; then
|
| 18 |
+
echo "Usage: $0 {submit|process}"
|
| 19 |
+
exit 1
|
| 20 |
+
fi
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
MODE="$1"
|
| 24 |
+
|
| 25 |
+
if [[ "$MODE" == "submit" ]]; then
|
| 26 |
+
|
| 27 |
+
mkdir -p "$OUT"
|
| 28 |
+
mkdir -p "$LOGS"
|
| 29 |
+
|
| 30 |
+
sbatch -A "$ACCOUNT" -t "$TIME" --export=ALL --array=1-$NUM_TASKS -o $LOGS/stdout.%A-%a.log -e $LOGS/stderr.%A-%a.log run_job_array.slurm \
|
| 31 |
+
"$DESIGN_SPEC" "$OUT" "$NUM_DESIGNS_PER_TASK" "$CONDA_ENVIRONMENT" --protocol protein-anything --cache "$CACHE_DIR"
|
| 32 |
+
squeue --me
|
| 33 |
+
|
| 34 |
+
elif [[ "$MODE" == "process" ]]; then
|
| 35 |
+
|
| 36 |
+
boltzgen merge "$OUT"/task-* --output "$MERGED_OUT"
|
| 37 |
+
boltzgen run "$DESIGN_SPEC" --steps filtering --protocol protein-anything --output "$MERGED_OUT"
|
| 38 |
+
|
| 39 |
+
else
|
| 40 |
+
echo "Usage: $0 {submit|process}"
|
| 41 |
+
exit 1
|
| 42 |
+
fi
|
scripts/slurm-example/run_job_array.slurm
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH -C gpu # Use GPU nodes
|
| 3 |
+
#SBATCH -q regular # Queue/partition
|
| 4 |
+
#SBATCH -t 03:00:00 # Walltime per array task
|
| 5 |
+
#SBATCH -N 1 # Number of nodes per task
|
| 6 |
+
#SBATCH --gpus-per-node=1 # 1 GPU per task
|
| 7 |
+
#SBATCH --array=1-2 # Job array indices. Override this.
|
| 8 |
+
#SBATCH --mem=64G
|
| 9 |
+
#SBATCH -J boltzgen
|
| 10 |
+
|
| 11 |
+
set -euo pipefail
|
| 12 |
+
set -x
|
| 13 |
+
|
| 14 |
+
echo "Starting job on host: $(hostname)"
|
| 15 |
+
echo "SLURM job ID: ${SLURM_JOB_ID:-unknown}"
|
| 16 |
+
echo "Array job ID: ${SLURM_ARRAY_JOB_ID:-unknown}"
|
| 17 |
+
echo "Array task ID: ${SLURM_ARRAY_TASK_ID:-unknown}"
|
| 18 |
+
echo "Nodes: ${SLURM_JOB_NUM_NODES}"
|
| 19 |
+
|
| 20 |
+
if [[ $# -lt 3 ]]; then
|
| 21 |
+
echo "Usage: $0 <design_spec> <outdir> <num_designs_per_job> <conda_environment>" >&2
|
| 22 |
+
exit 1
|
| 23 |
+
fi
|
| 24 |
+
|
| 25 |
+
design_spec="$1"
|
| 26 |
+
outdir="$2"
|
| 27 |
+
num_designs="${3}"
|
| 28 |
+
conda_environment="${4}"
|
| 29 |
+
shift 4 # Remove parsed args
|
| 30 |
+
extra_args=("$@") # Store everything else
|
| 31 |
+
|
| 32 |
+
echo "Design spec: $design_spec"
|
| 33 |
+
echo "Output dir: $outdir"
|
| 34 |
+
echo "Num designs: $num_designs"
|
| 35 |
+
echo "Conda environment: $conda_environment"
|
| 36 |
+
|
| 37 |
+
module load conda
|
| 38 |
+
conda activate "$conda_environment"
|
| 39 |
+
|
| 40 |
+
which python
|
| 41 |
+
which boltzgen
|
| 42 |
+
nvidia-smi
|
| 43 |
+
|
| 44 |
+
job_outdir="${outdir}/task-${SLURM_ARRAY_JOB_ID}-${SLURM_ARRAY_TASK_ID}"
|
| 45 |
+
mkdir -p "${job_outdir}"
|
| 46 |
+
|
| 47 |
+
srun --ntasks=1 --gpus-per-task=1 \
|
| 48 |
+
boltzgen run "$design_spec" \
|
| 49 |
+
--output "${job_outdir}" \
|
| 50 |
+
--num_designs "$num_designs" \
|
| 51 |
+
"${extra_args[@]}"
|
| 52 |
+
|
| 53 |
+
|
scripts/train.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run an official BoltzGen Hydra training configuration."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
MODEL_DIR = ROOT / "model"
|
| 13 |
+
sys.path.insert(0, str(MODEL_DIR))
|
| 14 |
+
os.environ["PYTHONPATH"] = os.pathsep.join(
|
| 15 |
+
[str(MODEL_DIR), os.environ.get("PYTHONPATH", "")]
|
| 16 |
+
).rstrip(os.pathsep)
|
| 17 |
+
|
| 18 |
+
from boltzgen.resources.main import main # noqa: E402
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def run() -> None:
|
| 22 |
+
if len(sys.argv) < 2:
|
| 23 |
+
raise SystemExit("Usage: python scripts/train.py CONFIG.yaml [key=value ...]")
|
| 24 |
+
main(sys.argv[1], sys.argv[2:])
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
run()
|