wuxing0105 commited on
Commit
1f88cea
·
verified ·
1 Parent(s): 6aab6b3

Upload folder using huggingface_hub (part 2)

Browse files
Files changed (38) hide show
  1. model/boltzgen/resources/config/train/boltzgen.no_distillation.yaml +484 -0
  2. model/boltzgen/resources/config/train/boltzgen.yaml +578 -0
  3. model/boltzgen/resources/config/train/boltzgen_small.yaml +346 -0
  4. model/boltzgen/resources/config/train/inverse_folding.yaml +376 -0
  5. model/boltzgen/resources/main.py +53 -0
  6. model/boltzgen/resources/metrics_normalization.json +38 -0
  7. model/boltzgen/resources/splits/val_ccd_pdb_pairs_boltzgen.txt +101 -0
  8. model/boltzgen/resources/splits/val_monomers_boltzgen_min50_max220.txt +110 -0
  9. model/boltzgen/resources/splits/validation_ids_boltz2_all.txt +398 -0
  10. model/boltzgen/task/__init__.py +0 -0
  11. model/boltzgen/task/analyze/analyze.py +1483 -0
  12. model/boltzgen/task/analyze/analyze_utils.py +1260 -0
  13. model/boltzgen/task/filter/__init__.py +0 -0
  14. model/boltzgen/task/filter/filter.py +1413 -0
  15. model/boltzgen/task/filter/seqplot_utils.py +505 -0
  16. model/boltzgen/task/predict/__init__.py +0 -0
  17. model/boltzgen/task/predict/data_from_generated.py +882 -0
  18. model/boltzgen/task/predict/data_from_yaml.py +449 -0
  19. model/boltzgen/task/predict/data_ligands.py +410 -0
  20. model/boltzgen/task/predict/data_protein_binder.py +608 -0
  21. model/boltzgen/task/predict/loading_utils.py +45 -0
  22. model/boltzgen/task/predict/predict.py +194 -0
  23. model/boltzgen/task/predict/writer.py +534 -0
  24. model/boltzgen/task/task.py +19 -0
  25. model/boltzgen/task/train/__init__.py +0 -0
  26. model/boltzgen/task/train/data.py +1259 -0
  27. model/boltzgen/task/train/train.py +250 -0
  28. model/boltzgen/utils/__init__.py +0 -0
  29. model/boltzgen/utils/pipeline_progress_bar.py +171 -0
  30. model/boltzgen/utils/quiet.py +16 -0
  31. requirements.txt +53 -0
  32. scripts/boltzgen.py +22 -0
  33. scripts/check_environment.py +55 -0
  34. scripts/filter.ipynb +0 -0
  35. scripts/slurm-example/README.md +28 -0
  36. scripts/slurm-example/run.sh +42 -0
  37. scripts/slurm-example/run_job_array.slurm +53 -0
  38. 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()