wuxing0105 commited on
Commit
b2cb4a0
·
verified ·
1 Parent(s): 69362c7

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. config/experiment/train_fsdp.yaml +54 -0
  2. config/model/architecture/foldingdit_1.1B.yaml +99 -0
  3. config/model/architecture/foldingdit_100M.yaml +101 -0
  4. config/model/architecture/foldingdit_360M.yaml +101 -0
  5. config/model/architecture/foldingdit_3B.yaml +99 -0
  6. config/model/architecture/foldingdit_700M.yaml +101 -0
  7. config/model/sampler/euler_maruyama.yaml +6 -0
  8. config/paths/default.yaml +20 -0
  9. examples/minimal.fasta +2 -0
  10. models/__init__.py +2 -0
  11. models/simplefold/__init__.py +4 -0
  12. models/simplefold/__pycache__/__init__.cpython-310.pyc +0 -0
  13. models/simplefold/__pycache__/__init__.cpython-311.pyc +0 -0
  14. models/simplefold/__pycache__/flow.cpython-311.pyc +0 -0
  15. models/simplefold/__pycache__/simplefold.cpython-311.pyc +0 -0
  16. models/simplefold/flow.py +101 -0
  17. models/simplefold/mlx/__init__.py +4 -0
  18. models/simplefold/mlx/__pycache__/architecture.cpython-310.pyc +0 -0
  19. models/simplefold/mlx/__pycache__/blocks.cpython-310.pyc +0 -0
  20. models/simplefold/mlx/__pycache__/confidence_module.cpython-310.pyc +0 -0
  21. models/simplefold/mlx/__pycache__/esm_modules.cpython-310.pyc +0 -0
  22. models/simplefold/mlx/__pycache__/esm_multihead_attention.cpython-310.pyc +0 -0
  23. models/simplefold/mlx/__pycache__/layers.cpython-310.pyc +0 -0
  24. models/simplefold/mlx/__pycache__/pos_embed.cpython-310.pyc +0 -0
  25. models/simplefold/mlx/architecture.py +333 -0
  26. models/simplefold/mlx/blocks.py +100 -0
  27. models/simplefold/mlx/confidence_module.py +84 -0
  28. models/simplefold/mlx/esm_modules.py +184 -0
  29. models/simplefold/mlx/esm_multihead_attention.py +224 -0
  30. models/simplefold/mlx/esm_network.py +160 -0
  31. models/simplefold/mlx/esm_rotary_embedding.py +74 -0
  32. models/simplefold/mlx/layers.py +263 -0
  33. models/simplefold/mlx/pos_embed.py +204 -0
  34. models/simplefold/mlx/sampler.py +112 -0
  35. models/simplefold/simplefold.py +781 -0
  36. models/simplefold/torch/__init__.py +4 -0
  37. models/simplefold/torch/architecture.py +304 -0
  38. models/simplefold/torch/layers.py +313 -0
  39. models/simplefold/torch/pos_embed.py +177 -0
  40. scripts/__pycache__/inference.cpython-311.pyc +0 -0
  41. scripts/__pycache__/train.cpython-310.pyc +0 -0
  42. scripts/__pycache__/train_fsdp.cpython-310.pyc +0 -0
  43. scripts/evaluate.py +101 -0
  44. scripts/inference.py +420 -0
  45. scripts/preflight.py +153 -0
  46. scripts/process_data.py +73 -0
  47. scripts/run_inference.py +41 -0
  48. scripts/tokenize_data.py +58 -0
  49. scripts/train.py +142 -0
  50. scripts/train_fsdp.py +161 -0
config/experiment/train_fsdp.yaml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+
3
+ # All parameters below will be merged with parameters from default configurations set above
4
+ # this allows you to overwrite only specified parameters
5
+
6
+ # Options for training:
7
+ # - /data: [pdb, pdb_sp, pdb_sp_afesm, pdb_sp_afesme]
8
+ # - /model/architecture:
9
+ # [foldingdit_100M, foldingdit_360M, foldingdit_700M,
10
+ # foldingdit_1.1B, foldingdit_1.6B, foldingdit_3B]
11
+ # - /trainer: [default, fsdp]
12
+
13
+ defaults:
14
+ - override /data: pdb
15
+ - override /model: simplefold
16
+ - override /model/architecture: foldingdit_100M
17
+ - override /model/sampler: euler_maruyama
18
+ - override /model/processor: protein_processor
19
+ - override /callbacks: default
20
+ - override /trainer: fsdp # FSDP training
21
+ - override /logger: tensorboard
22
+
23
+ # load_ckpt_path: [PATH_TO_YOUR_CKPT] # e.g. uncomment to load a checkpoint
24
+ seed: 12345
25
+
26
+ trainer:
27
+ max_steps: 300000
28
+ val_check_interval: 10000 # this controls BOTH checkpoint steps and validation
29
+ accumulate_grad_batches: 1 # always 1 for FSDP
30
+
31
+ data:
32
+ batch_size: 8 # should be the same as the number of GPUs
33
+ num_workers: 16
34
+
35
+ model:
36
+ _target_: models.simplefold.simplefold.SimpleFold
37
+ ema_decay: 0.999
38
+ clip_grad_norm_val: 2.0
39
+ esm_model: "esm2_3B"
40
+
41
+ scheduler:
42
+ _target_: onescience.utils.simplefold.lr_scheduler.LinearWarmup
43
+ _partial_: true
44
+ min_lr: 1e-6
45
+ max_lr: ${model.optimizer.lr}
46
+ warmup_steps: 5000
47
+
48
+ architecture:
49
+ esm_model: ${model.esm_model}
50
+
51
+ processor:
52
+ scale: 16.0
53
+ ref_scale: 5.0
54
+ multiplicity: 16 # number of copies per GPU
config/model/architecture/foldingdit_1.1B.yaml ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: models.simplefold.torch.architecture.FoldingDiT
2
+
3
+ hidden_size: 1280
4
+ num_heads: 20
5
+ atom_num_heads: 6
6
+ output_channels: 3
7
+ use_atom_mask: False
8
+ use_length_condition: True
9
+ esm_dropout_prob: 0.0
10
+ esm_model: esm2_3B
11
+
12
+ time_embedder:
13
+ _target_: models.simplefold.torch.layers.TimestepEmbedder
14
+ hidden_size: 1280
15
+ aminoacid_pos_embedder:
16
+ _target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
17
+ in_dim: 1
18
+ embed_dim: 1280
19
+ include_input: True
20
+ pos_embedder:
21
+ _target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
22
+ in_dim: 3
23
+ include_input: True
24
+ min_freq_log2: 0
25
+ max_freq_log2: 12
26
+ num_freqs: 128
27
+ log_sampling: True
28
+
29
+ trunk:
30
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
31
+ depth: 36
32
+ block:
33
+ _target_: models.simplefold.torch.blocks.DiTBlock
34
+ _partial_: True # because in the for loop we create a new module
35
+ hidden_size: 1280
36
+ mlp_ratio: 4.0
37
+ use_swiglu: True # SwiGLU FFN
38
+ self_attention_layer:
39
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
40
+ _partial_: True
41
+ hidden_size: 1280
42
+ num_heads: 20
43
+ qk_norm: True
44
+ pos_embedder:
45
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
46
+ in_dim: 4
47
+ embed_dim: 1280
48
+ num_heads: 20
49
+ base: 100.0
50
+
51
+ atom_hidden_size_enc: 384
52
+ atom_n_queries_enc: 32
53
+ atom_n_keys_enc: 128
54
+ atom_encoder_transformer:
55
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
56
+ depth: 2
57
+ block:
58
+ _target_: models.simplefold.torch.blocks.DiTBlock
59
+ _partial_: True # because in the for loop we create a new module
60
+ hidden_size: 384
61
+ mlp_ratio: 4.0
62
+ use_swiglu: True # SwiGLU FFN
63
+ self_attention_layer:
64
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
65
+ _partial_: True
66
+ hidden_size: 384
67
+ num_heads: 6
68
+ qk_norm: True
69
+ pos_embedder:
70
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
71
+ in_dim: 4
72
+ embed_dim: 384
73
+ num_heads: 6
74
+ base: 100.0
75
+
76
+ atom_hidden_size_dec: 384
77
+ atom_n_queries_dec: 32
78
+ atom_n_keys_dec: 128
79
+ atom_decoder_transformer:
80
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
81
+ depth: 2
82
+ block:
83
+ _target_: models.simplefold.torch.blocks.DiTBlock
84
+ _partial_: True # because in the for loop we create a new module
85
+ hidden_size: 384
86
+ mlp_ratio: 4.0
87
+ use_swiglu: True # SwiGLU FFN
88
+ self_attention_layer:
89
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
90
+ _partial_: True
91
+ hidden_size: 384
92
+ num_heads: 6
93
+ qk_norm: True
94
+ pos_embedder:
95
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
96
+ in_dim: 4
97
+ embed_dim: 384
98
+ num_heads: 6
99
+ base: 100.0
config/model/architecture/foldingdit_100M.yaml ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: models.simplefold.torch.architecture.FoldingDiT
2
+
3
+ hidden_size: 768
4
+ num_heads: 12
5
+ atom_num_heads: 4
6
+ output_channels: 3
7
+ use_atom_mask: False
8
+ use_length_condition: True
9
+ esm_dropout_prob: 0.0
10
+ esm_model: esm2_3B
11
+
12
+ time_embedder:
13
+ _target_: models.simplefold.torch.layers.TimestepEmbedder
14
+ hidden_size: 768
15
+
16
+ aminoacid_pos_embedder:
17
+ _target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
18
+ in_dim: 1
19
+ embed_dim: 768
20
+ include_input: True
21
+
22
+ pos_embedder:
23
+ _target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
24
+ in_dim: 3
25
+ include_input: True
26
+ min_freq_log2: 0
27
+ max_freq_log2: 12
28
+ num_freqs: 128
29
+ log_sampling: True
30
+
31
+ trunk:
32
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
33
+ depth: 8
34
+ block:
35
+ _target_: models.simplefold.torch.blocks.DiTBlock
36
+ _partial_: True # because in the for loop we create a new module
37
+ hidden_size: 768
38
+ mlp_ratio: 4.0
39
+ use_swiglu: True # SwiGLU FFN
40
+ self_attention_layer:
41
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
42
+ _partial_: True
43
+ hidden_size: 768
44
+ num_heads: 12
45
+ qk_norm: True
46
+ pos_embedder:
47
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
48
+ in_dim: 4
49
+ embed_dim: 768
50
+ num_heads: 12
51
+ base: 100.0
52
+
53
+ atom_hidden_size_enc: 256
54
+ atom_n_queries_enc: 32
55
+ atom_n_keys_enc: 128
56
+ atom_encoder_transformer:
57
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
58
+ depth: 1
59
+ block:
60
+ _target_: models.simplefold.torch.blocks.DiTBlock
61
+ _partial_: True # because in the for loop we create a new module
62
+ hidden_size: 256
63
+ mlp_ratio: 4.0
64
+ use_swiglu: True # SwiGLU FFN
65
+ self_attention_layer:
66
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
67
+ _partial_: True
68
+ hidden_size: 256
69
+ num_heads: 4
70
+ qk_norm: True
71
+ pos_embedder:
72
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
73
+ in_dim: 4
74
+ embed_dim: 256
75
+ num_heads: 4
76
+ base: 100.0
77
+
78
+ atom_hidden_size_dec: 256
79
+ atom_n_queries_dec: 32
80
+ atom_n_keys_dec: 128
81
+ atom_decoder_transformer:
82
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
83
+ depth: 1
84
+ block:
85
+ _target_: models.simplefold.torch.blocks.DiTBlock
86
+ _partial_: True # because in the for loop we create a new module
87
+ hidden_size: 256
88
+ mlp_ratio: 4.0
89
+ use_swiglu: True # SwiGLU FFN
90
+ self_attention_layer:
91
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
92
+ _partial_: True
93
+ hidden_size: 256
94
+ num_heads: 4
95
+ qk_norm: True
96
+ pos_embedder:
97
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
98
+ in_dim: 4
99
+ embed_dim: 256
100
+ num_heads: 4
101
+ base: 100.0
config/model/architecture/foldingdit_360M.yaml ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: models.simplefold.torch.architecture.FoldingDiT
2
+
3
+ hidden_size: 1024
4
+ num_heads: 16
5
+ atom_num_heads: 4
6
+ output_channels: 3
7
+ use_atom_mask: False
8
+ use_length_condition: True
9
+ esm_dropout_prob: 0.0
10
+ esm_model: esm2_3B
11
+
12
+ time_embedder:
13
+ _target_: models.simplefold.torch.layers.TimestepEmbedder
14
+ hidden_size: 1024
15
+
16
+ aminoacid_pos_embedder:
17
+ _target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
18
+ in_dim: 1
19
+ embed_dim: 1024
20
+ include_input: True
21
+
22
+ pos_embedder:
23
+ _target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
24
+ in_dim: 3
25
+ include_input: True
26
+ min_freq_log2: 0
27
+ max_freq_log2: 12
28
+ num_freqs: 128
29
+ log_sampling: True
30
+
31
+ trunk:
32
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
33
+ depth: 18
34
+ block:
35
+ _target_: models.simplefold.torch.blocks.DiTBlock
36
+ _partial_: True # because in the for loop we create a new module
37
+ hidden_size: 1024
38
+ mlp_ratio: 4.0
39
+ use_swiglu: True # SwiGLU FFN
40
+ self_attention_layer:
41
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
42
+ _partial_: True
43
+ hidden_size: 1024
44
+ num_heads: 16
45
+ qk_norm: True
46
+ pos_embedder:
47
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
48
+ in_dim: 4
49
+ embed_dim: 1024
50
+ num_heads: 16
51
+ base: 100.0
52
+
53
+ atom_hidden_size_enc: 256
54
+ atom_n_queries_enc: 32
55
+ atom_n_keys_enc: 128
56
+ atom_encoder_transformer:
57
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
58
+ depth: 2
59
+ block:
60
+ _target_: models.simplefold.torch.blocks.DiTBlock
61
+ _partial_: True # because in the for loop we create a new module
62
+ hidden_size: 256
63
+ mlp_ratio: 4.0
64
+ use_swiglu: True # SwiGLU FFN
65
+ self_attention_layer:
66
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
67
+ _partial_: True
68
+ hidden_size: 256
69
+ num_heads: 4
70
+ qk_norm: True
71
+ pos_embedder:
72
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
73
+ in_dim: 4
74
+ embed_dim: 256
75
+ num_heads: 4
76
+ base: 100.0
77
+
78
+ atom_hidden_size_dec: 256
79
+ atom_n_queries_dec: 32
80
+ atom_n_keys_dec: 128
81
+ atom_decoder_transformer:
82
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
83
+ depth: 2
84
+ block:
85
+ _target_: models.simplefold.torch.blocks.DiTBlock
86
+ _partial_: True # because in the for loop we create a new module
87
+ hidden_size: 256
88
+ mlp_ratio: 4.0
89
+ use_swiglu: True # SwiGLU FFN
90
+ self_attention_layer:
91
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
92
+ _partial_: True
93
+ hidden_size: 256
94
+ num_heads: 4
95
+ qk_norm: True
96
+ pos_embedder:
97
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
98
+ in_dim: 4
99
+ embed_dim: 256
100
+ num_heads: 4
101
+ base: 100.0
config/model/architecture/foldingdit_3B.yaml ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: models.simplefold.torch.architecture.FoldingDiT
2
+
3
+ hidden_size: 2048
4
+ num_heads: 32
5
+ atom_num_heads: 10
6
+ output_channels: 3
7
+ use_atom_mask: False
8
+ use_length_condition: True
9
+ esm_dropout_prob: 0.0
10
+ esm_model: esm2_3B
11
+
12
+ time_embedder:
13
+ _target_: models.simplefold.torch.layers.TimestepEmbedder
14
+ hidden_size: 2048
15
+ aminoacid_pos_embedder:
16
+ _target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
17
+ in_dim: 1
18
+ embed_dim: 2048
19
+ include_input: True
20
+ pos_embedder:
21
+ _target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
22
+ in_dim: 3
23
+ include_input: True
24
+ min_freq_log2: 0
25
+ max_freq_log2: 12
26
+ num_freqs: 128
27
+ log_sampling: True
28
+
29
+ trunk:
30
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
31
+ depth: 36
32
+ block:
33
+ _target_: models.simplefold.torch.blocks.DiTBlock
34
+ _partial_: True # because in the for loop we create a new module
35
+ hidden_size: 2048
36
+ mlp_ratio: 4.0
37
+ use_swiglu: True # SwiGLU FFN
38
+ self_attention_layer:
39
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
40
+ _partial_: True
41
+ hidden_size: 2048
42
+ num_heads: 32
43
+ qk_norm: True
44
+ pos_embedder:
45
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
46
+ in_dim: 4
47
+ embed_dim: 2048
48
+ num_heads: 32
49
+ base: 100.0
50
+
51
+ atom_hidden_size_enc: 640
52
+ atom_n_queries_enc: 32
53
+ atom_n_keys_enc: 128
54
+ atom_encoder_transformer:
55
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
56
+ depth: 4
57
+ block:
58
+ _target_: models.simplefold.torch.blocks.DiTBlock
59
+ _partial_: True # because in the for loop we create a new module
60
+ hidden_size: 640
61
+ mlp_ratio: 4.0
62
+ use_swiglu: True # SwiGLU FFN
63
+ self_attention_layer:
64
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
65
+ _partial_: True
66
+ hidden_size: 640
67
+ num_heads: 10
68
+ qk_norm: True
69
+ pos_embedder:
70
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
71
+ in_dim: 4
72
+ embed_dim: 640
73
+ num_heads: 10
74
+ base: 100.0
75
+
76
+ atom_hidden_size_dec: 640
77
+ atom_n_queries_dec: 32
78
+ atom_n_keys_dec: 128
79
+ atom_decoder_transformer:
80
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
81
+ depth: 4
82
+ block:
83
+ _target_: models.simplefold.torch.blocks.DiTBlock
84
+ _partial_: True # because in the for loop we create a new module
85
+ hidden_size: 640
86
+ mlp_ratio: 4.0
87
+ use_swiglu: True # SwiGLU FFN
88
+ self_attention_layer:
89
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
90
+ _partial_: True
91
+ hidden_size: 640
92
+ num_heads: 10
93
+ qk_norm: True
94
+ pos_embedder:
95
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
96
+ in_dim: 4
97
+ embed_dim: 640
98
+ num_heads: 10
99
+ base: 100.0
config/model/architecture/foldingdit_700M.yaml ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: models.simplefold.torch.architecture.FoldingDiT
2
+
3
+ hidden_size: 1152
4
+ num_heads: 16
5
+ atom_num_heads: 4
6
+ output_channels: 3
7
+ use_atom_mask: False
8
+ use_length_condition: True
9
+ esm_dropout_prob: 0.0
10
+ esm_model: esm2_3B
11
+
12
+ time_embedder:
13
+ _target_: models.simplefold.torch.layers.TimestepEmbedder
14
+ hidden_size: 1152
15
+
16
+ aminoacid_pos_embedder:
17
+ _target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
18
+ in_dim: 1
19
+ embed_dim: 1152
20
+ include_input: True
21
+
22
+ pos_embedder:
23
+ _target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
24
+ in_dim: 3
25
+ include_input: True
26
+ min_freq_log2: 0
27
+ max_freq_log2: 12
28
+ num_freqs: 128
29
+ log_sampling: True
30
+
31
+ trunk:
32
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
33
+ depth: 28
34
+ block:
35
+ _target_: models.simplefold.torch.blocks.DiTBlock
36
+ _partial_: True # because in the for loop we create a new module
37
+ hidden_size: 1152
38
+ mlp_ratio: 4.0
39
+ use_swiglu: True # SwiGLU FFN
40
+ self_attention_layer:
41
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
42
+ _partial_: True
43
+ hidden_size: 1152
44
+ num_heads: 16
45
+ qk_norm: True
46
+ pos_embedder:
47
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
48
+ in_dim: 4
49
+ embed_dim: 1152
50
+ num_heads: 16
51
+ base: 100.0
52
+
53
+ atom_hidden_size_enc: 256
54
+ atom_n_queries_enc: 32
55
+ atom_n_keys_enc: 128
56
+ atom_encoder_transformer:
57
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
58
+ depth: 2
59
+ block:
60
+ _target_: models.simplefold.torch.blocks.DiTBlock
61
+ _partial_: True # because in the for loop we create a new module
62
+ hidden_size: 256
63
+ mlp_ratio: 4.0
64
+ use_swiglu: True # SwiGLU FFN
65
+ self_attention_layer:
66
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
67
+ _partial_: True
68
+ hidden_size: 256
69
+ num_heads: 4
70
+ qk_norm: True
71
+ pos_embedder:
72
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
73
+ in_dim: 4
74
+ embed_dim: 256
75
+ num_heads: 4
76
+ base: 100.0
77
+
78
+ atom_hidden_size_dec: 256
79
+ atom_n_queries_dec: 32
80
+ atom_n_keys_dec: 128
81
+ atom_decoder_transformer:
82
+ _target_: models.simplefold.torch.blocks.HomogenTrunk
83
+ depth: 2
84
+ block:
85
+ _target_: models.simplefold.torch.blocks.DiTBlock
86
+ _partial_: True # because in the for loop we create a new module
87
+ hidden_size: 256
88
+ mlp_ratio: 4.0
89
+ use_swiglu: True # SwiGLU FFN
90
+ self_attention_layer:
91
+ _target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
92
+ _partial_: True
93
+ hidden_size: 256
94
+ num_heads: 4
95
+ qk_norm: True
96
+ pos_embedder:
97
+ _target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
98
+ in_dim: 4
99
+ embed_dim: 256
100
+ num_heads: 4
101
+ base: 100.0
config/model/sampler/euler_maruyama.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ _target_: models.simplefold.torch.sampler.EMSampler
2
+ num_timesteps: 500
3
+ t_start: 1e-4
4
+ tau: 0.3
5
+ log_timesteps: True
6
+ w_cutoff: 0.99
config/paths/default.yaml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # path to root directory
2
+ # this requires PROJECT_ROOT environment variable to exist
3
+ # you can replace it with "." if you want the root to be the current working directory
4
+ root_dir: "./"
5
+
6
+ tmp_dir: ${paths.root_dir}tmp/
7
+
8
+ # path to data directory
9
+ data_dir: ${paths.root_dir}data/
10
+
11
+ # path to logging directory
12
+ log_dir: ${paths.root_dir}logs/
13
+
14
+ # artifacts directory
15
+ output_dir: ${paths.root_dir}artifacts/
16
+
17
+ sample_dir: ${.output_dir}/samples/
18
+
19
+ # path to working directory
20
+ work_dir: ${hydra:runtime.cwd}
examples/minimal.fasta ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ >simplefold_minimal_example
2
+ GASKLRAVLEKLKLSRDDISTAAGMVKGVVDHLLLRLKCDSAFRGVGLLNTGSYYEHVKISAPNEFDVMFKLEVPRIQLEEYSNTRAYYFVKFKRNPKENPLSQFLEGEILSASKMLSKFRKIIKEEINDDTDVIMKRKRGGSPAVTLLISEKISVDITLALESKSSWPASTQEGLRIQNWLSAKVRKQLRLKPFYLVPKHAEETWRLSFSHIEKEILNNHGKSKTCCENKEEKCCRKDCLKLMKYLLEQLKERFKDKKHLDKFSSYHVKTAFFHVCTQNPQDSQWDRKDLGLCFDNCVTYFLQCLRTEKLENYFIPEFNLFSSNLIDKRSKEFLTKQIEYERNNEFPVFD
models/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Bundled model modules."""
2
+
models/simplefold/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
models/simplefold/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (165 Bytes). View file
 
models/simplefold/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (181 Bytes). View file
 
models/simplefold/__pycache__/flow.cpython-311.pyc ADDED
Binary file (5.76 kB). View file
 
models/simplefold/__pycache__/simplefold.cpython-311.pyc ADDED
Binary file (33.6 kB). View file
 
models/simplefold/flow.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ def right_pad_dims_to(x, t):
7
+ padding_dims = x.ndim - t.ndim
8
+ if padding_dims <= 0:
9
+ return t
10
+ return t.reshape(*t.shape, *((1,) * padding_dims))
11
+
12
+
13
+ class BasePath:
14
+ """base class for flow matching path"""
15
+
16
+ def __init__(self):
17
+ return
18
+
19
+ def compute_alpha_t(self, t):
20
+ """Compute the data coefficient along the path"""
21
+ return None, None
22
+
23
+ def compute_sigma_t(self, t):
24
+ """Compute the noise coefficient along the path"""
25
+ return None, None
26
+
27
+ def compute_d_alpha_alpha_ratio_t(self, t):
28
+ """Compute the ratio between d_alpha and alpha"""
29
+ alpha_t, d_alpha_t = self.compute_alpha_t(t)
30
+ return d_alpha_t / alpha_t
31
+
32
+ def compute_mu_t(self, t, x0, x1):
33
+ """Compute the mean of time-dependent density p_t"""
34
+ alpha_t, _ = self.compute_alpha_t(t)
35
+ sigma_t, _ = self.compute_sigma_t(t)
36
+ return alpha_t * x1 + sigma_t * x0
37
+
38
+ def compute_xt(self, t, x0, x1):
39
+ """Sample xt from time-dependent density p_t; rng is required"""
40
+ xt = self.compute_mu_t(t, x0, x1)
41
+ return xt
42
+
43
+ def compute_ut(self, t, x0, x1):
44
+ """Compute the vector field corresponding to p_t"""
45
+ _, d_alpha_t = self.compute_alpha_t(t)
46
+ _, d_sigma_t = self.compute_sigma_t(t)
47
+ return d_alpha_t * x1 + d_sigma_t * x0
48
+
49
+ def interpolant(self, t, x0, x1):
50
+ t = right_pad_dims_to(x0, t)
51
+ xt = self.compute_xt(t, x0, x1)
52
+ ut = self.compute_ut(t, x0, x1)
53
+ return t, xt, ut
54
+
55
+ def compute_drift(self, x, t):
56
+ """We always output sde according to score parametrization; """
57
+ t = right_pad_dims_to(x, t)
58
+ alpha_ratio = self.compute_d_alpha_alpha_ratio_t(t)
59
+ sigma_t, d_sigma_t = self.compute_sigma_t(t)
60
+ drift_mean = alpha_ratio * x
61
+ drift_var = alpha_ratio * (sigma_t ** 2) - sigma_t * d_sigma_t
62
+ return -drift_mean, drift_var
63
+
64
+ def compute_score_from_velocity(self, v_t, y_t, t):
65
+ t = right_pad_dims_to(y_t, t)
66
+ alpha_t, d_alpha_t = self.compute_alpha_t(t)
67
+ sigma_t, d_sigma_t = self.compute_sigma_t(t)
68
+ mean = y_t
69
+ reverse_alpha_ratio = alpha_t / d_alpha_t
70
+ var = sigma_t**2 - reverse_alpha_ratio * d_sigma_t * sigma_t
71
+ score = (reverse_alpha_ratio * v_t - mean) / var
72
+ return score
73
+
74
+ def compute_velocity_from_score(self, s_t, y_t, t):
75
+ t = right_pad_dims_to(y_t, t)
76
+ drift_mean, drift_var = self.compute_drift(y_t, t)
77
+ velocity = -drift_mean + drift_var * s_t
78
+ return velocity
79
+
80
+
81
+ class LinearPath(BasePath):
82
+ """
83
+ Linear flow process:
84
+ x0: noise, x1: data
85
+ In inference, we sample data from 0 -> 1
86
+ """
87
+
88
+ def __init__(self):
89
+ super().__init__()
90
+
91
+ def compute_alpha_t(self, t):
92
+ """Compute the data coefficient along the path"""
93
+ return t, 1
94
+
95
+ def compute_sigma_t(self, t):
96
+ """Compute the noise coefficient along the path"""
97
+ return 1 - t, -1
98
+
99
+ def compute_d_alpha_alpha_ratio_t(self, t):
100
+ """Compute the ratio between d_alpha and alpha"""
101
+ return 1 / t
models/simplefold/mlx/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
models/simplefold/mlx/__pycache__/architecture.cpython-310.pyc ADDED
Binary file (6.66 kB). View file
 
models/simplefold/mlx/__pycache__/blocks.cpython-310.pyc ADDED
Binary file (2.92 kB). View file
 
models/simplefold/mlx/__pycache__/confidence_module.cpython-310.pyc ADDED
Binary file (1.92 kB). View file
 
models/simplefold/mlx/__pycache__/esm_modules.cpython-310.pyc ADDED
Binary file (5.6 kB). View file
 
models/simplefold/mlx/__pycache__/esm_multihead_attention.cpython-310.pyc ADDED
Binary file (4.84 kB). View file
 
models/simplefold/mlx/__pycache__/layers.cpython-310.pyc ADDED
Binary file (8.02 kB). View file
 
models/simplefold/mlx/__pycache__/pos_embed.cpython-310.pyc ADDED
Binary file (5.67 kB). View file
 
models/simplefold/mlx/architecture.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import math
7
+ import mlx.nn as nn
8
+ import mlx.core as mx
9
+ from models.simplefold.mlx.layers import FinalLayer, ConditionEmbedder
10
+ from onescience.utils.simplefold.esm_utils import esm_model_dict
11
+
12
+
13
+ # MLX does not have a native one_hot implementation
14
+ def one_hot(indices, num_classes, dtype=None):
15
+ """
16
+ MLX version of torch.one_hot.
17
+
18
+ Args:
19
+ indices: integer MLX array of any shape, containing class indices in [0, num_classes).
20
+ num_classes: number of classes for the one-hot dimension.
21
+ dtype: output data type (defaults to float32).
22
+
23
+ Returns:
24
+ MLX array of shape indices.shape + (num_classes,) and given dtype.
25
+ """
26
+ # Default to float32 if no dtype is given
27
+ if dtype is None:
28
+ dtype = mx.float32
29
+
30
+ classes = mx.arange(num_classes, dtype=indices.dtype)
31
+
32
+ # Broadcast-compare: result has shape indices.shape + (num_classes,)
33
+ # For each position, only the matched class index gives True
34
+ mask = indices[..., mx.newaxis] == classes
35
+
36
+ # Cast boolean mask to desired dtype
37
+ return mask.astype(dtype)
38
+
39
+
40
+ class FoldingDiT(nn.Module):
41
+ def __init__(
42
+ self,
43
+ trunk,
44
+ time_embedder,
45
+ aminoacid_pos_embedder,
46
+ pos_embedder,
47
+ atom_encoder_transformer,
48
+ atom_decoder_transformer,
49
+ hidden_size=1152,
50
+ num_heads=16,
51
+ atom_num_heads=4,
52
+ output_channels=3,
53
+ atom_hidden_size_enc=256,
54
+ atom_hidden_size_dec=256,
55
+ atom_n_queries_enc=32,
56
+ atom_n_keys_enc=128,
57
+ atom_n_queries_dec=32,
58
+ atom_n_keys_dec=128,
59
+ esm_model="esm2_3B",
60
+ esm_dropout_prob=0.0,
61
+ use_atom_mask=False,
62
+ use_length_condition=True,
63
+ ):
64
+ super().__init__()
65
+ self.pos_embedder = pos_embedder
66
+ pos_embed_channels = pos_embedder.embed_dim
67
+ self.aminoacid_pos_embedder = aminoacid_pos_embedder
68
+ aminoacid_pos_embed_channels = aminoacid_pos_embedder.embed_dim
69
+
70
+ self.time_embedder = time_embedder
71
+
72
+ self.atom_encoder_transformer = atom_encoder_transformer
73
+ self.atom_decoder_transformer = atom_decoder_transformer
74
+
75
+ self.trunk = trunk
76
+
77
+ self.hidden_size = hidden_size
78
+ self.output_channels = output_channels
79
+ self.num_heads = num_heads
80
+ self.atom_num_heads = atom_num_heads
81
+ self.use_atom_mask = use_atom_mask
82
+ self.esm_dropout_prob = esm_dropout_prob
83
+ self.use_length_condition = use_length_condition
84
+
85
+ esm_s_dim = esm_model_dict[esm_model]["esm_s_dim"]
86
+ esm_num_layers = esm_model_dict[esm_model]["esm_num_layers"]
87
+
88
+ self.atom_hidden_size_enc = atom_hidden_size_enc
89
+ self.atom_hidden_size_dec = atom_hidden_size_dec
90
+ self.atom_n_queries_enc = atom_n_queries_enc
91
+ self.atom_n_keys_enc = atom_n_keys_enc
92
+ self.atom_n_queries_dec = atom_n_queries_dec
93
+ self.atom_n_keys_dec = atom_n_keys_dec
94
+
95
+ atom_feat_dim = pos_embed_channels + aminoacid_pos_embed_channels + 427
96
+ self.atom_feat_proj = nn.Sequential(
97
+ nn.Linear(atom_feat_dim, hidden_size),
98
+ nn.LayerNorm(hidden_size),
99
+ nn.SiLU(),
100
+ )
101
+ self.atom_pos_proj = nn.Linear(pos_embed_channels, hidden_size, bias=False)
102
+
103
+ if self.use_length_condition:
104
+ self.length_embedder = nn.Sequential(
105
+ nn.Linear(1, hidden_size, bias=False),
106
+ nn.LayerNorm(hidden_size),
107
+ )
108
+
109
+ self.atom_in_proj = nn.Linear(hidden_size * 2, hidden_size, bias=False)
110
+
111
+ self.esm_s_combine = mx.zeros(esm_num_layers)
112
+ self.esm_s_proj = ConditionEmbedder(
113
+ input_dim=esm_s_dim,
114
+ hidden_size=hidden_size,
115
+ dropout_prob=0,
116
+ )
117
+
118
+ latent_cat_dim = hidden_size * 2
119
+ self.esm_cat_proj = nn.Linear(latent_cat_dim, hidden_size)
120
+
121
+ self.context2atom_proj = nn.Sequential(
122
+ nn.Linear(hidden_size, self.atom_hidden_size_enc),
123
+ nn.LayerNorm(self.atom_hidden_size_enc),
124
+ )
125
+
126
+ self.atom_enc_cond_proj = nn.Sequential(
127
+ nn.Linear(hidden_size, self.atom_hidden_size_enc),
128
+ nn.LayerNorm(self.atom_hidden_size_enc),
129
+ )
130
+
131
+ self.atom2latent_proj = nn.Sequential(
132
+ nn.Linear(self.atom_hidden_size_enc, hidden_size),
133
+ nn.LayerNorm(hidden_size),
134
+ )
135
+
136
+ self.atom_dec_cond_proj = nn.Sequential(
137
+ nn.Linear(hidden_size, self.atom_hidden_size_dec),
138
+ nn.LayerNorm(self.atom_hidden_size_dec),
139
+ )
140
+
141
+ self.latent2atom_proj = nn.Sequential(
142
+ nn.Linear(hidden_size, hidden_size),
143
+ nn.SiLU(),
144
+ nn.LayerNorm(hidden_size),
145
+ nn.Linear(hidden_size, self.atom_hidden_size_dec),
146
+ )
147
+
148
+ self.final_layer = FinalLayer(
149
+ self.atom_hidden_size_dec, output_channels, c_dim=hidden_size
150
+ )
151
+
152
+ def create_local_attn_bias(
153
+ self,
154
+ n: int,
155
+ n_queries: int,
156
+ n_keys: int,
157
+ inf: float = 1e10,
158
+ ):
159
+ """Create local attention bias based on query window n_queries and kv window n_keys.
160
+
161
+ Args:
162
+ n (int): the length of quiries
163
+ n_queries (int): window size of quiries
164
+ n_keys (int): window size of keys/values
165
+ inf (float, optional): the inf to mask attention. Defaults to 1e10.
166
+ device (torch.device, optional): cuda|cpu|None. Defaults to None.
167
+
168
+ Returns:
169
+ torch.Tensor: the diagonal-like global attention bias
170
+ """
171
+ n_trunks = int(math.ceil(n / n_queries))
172
+ padded_n = n_trunks * n_queries
173
+ attn_mask = mx.zeros((padded_n, padded_n))
174
+ for block_index in range(0, n_trunks):
175
+ i = block_index * n_queries
176
+ j1 = max(0, n_queries * block_index - (n_keys - n_queries) // 2)
177
+ j2 = n_queries * block_index + (n_queries + n_keys) // 2
178
+ attn_mask[i : i + n_queries, j1:j2] = 1.0
179
+ attn_bias = (1 - attn_mask) * -inf
180
+ return attn_bias[:n, :n]
181
+
182
+ def create_atom_attn_mask(
183
+ self, feats, natoms, atom_n_queries=None, atom_n_keys=None, inf: float = 1e10
184
+ ):
185
+
186
+ if atom_n_queries is not None and atom_n_keys is not None:
187
+ atom_attn_mask = self.create_local_attn_bias(
188
+ n=natoms, n_queries=atom_n_queries, n_keys=atom_n_keys, inf=inf
189
+ )
190
+ else:
191
+ atom_attn_mask = None
192
+
193
+ return atom_attn_mask
194
+
195
+ def __call__(self, noised_pos, t, feats, self_cond=None):
196
+
197
+ B, N, _ = feats["ref_pos"].shape
198
+ M = feats["mol_type"].shape[1]
199
+ atom_to_token = feats["atom_to_token"].astype(mx.float32)
200
+ atom_to_token_idx = feats["atom_to_token_idx"]
201
+ ref_space_uid = feats["ref_space_uid"]
202
+
203
+ # create atom attention masks
204
+ atom_attn_mask_enc = self.create_atom_attn_mask(
205
+ feats,
206
+ natoms=N,
207
+ atom_n_queries=self.atom_n_queries_enc,
208
+ atom_n_keys=self.atom_n_keys_enc,
209
+ )
210
+ atom_attn_mask_dec = self.create_atom_attn_mask(
211
+ feats,
212
+ natoms=N,
213
+ atom_n_queries=self.atom_n_queries_dec,
214
+ atom_n_keys=self.atom_n_keys_dec,
215
+ )
216
+
217
+ # create condition embeddings for AdaLN
218
+ c_emb = self.time_embedder(t) # (B, D)
219
+ if self.use_length_condition:
220
+ length = feats["max_num_tokens"].astype(mx.float32)[..., None]
221
+ c_emb = c_emb + self.length_embedder(mx.log(length))
222
+
223
+ mol_type = feats["mol_type"]
224
+ mol_type = one_hot(mol_type, num_classes=4).astype(mx.float32) # [B, M, 4]
225
+ res_type = feats["res_type"].astype(mx.float32) # [B, M, 33]
226
+ pocket_feature = feats["pocket_feature"].astype(mx.float32) # [B, M, 4]
227
+ res_feat = mx.concatenate(
228
+ [mol_type, res_type, pocket_feature], axis=-1
229
+ ) # [B, M, 41]
230
+ atom_feat_from_res = mx.matmul(atom_to_token, res_feat) # [B, N, 41]
231
+ atom_res_pos = self.aminoacid_pos_embedder(
232
+ pos=atom_to_token_idx[..., None].astype(mx.float32)
233
+ )
234
+ ref_pos_emb = self.pos_embedder(pos=feats["ref_pos"])
235
+ atom_feat = mx.concatenate(
236
+ [
237
+ ref_pos_emb, # (B, N, PD1)
238
+ atom_feat_from_res, # (B, N, 41)
239
+ atom_res_pos, # (B, N, PD2)
240
+ feats["ref_charge"][..., None], # (B, N, 1)
241
+ feats["atom_pad_mask"][..., None], # (B, N, 1)
242
+ feats["ref_element"], # (B, N, 128)
243
+ feats["ref_atom_name_chars"].reshape(B, N, 4 * 64), # (B, N, 256)
244
+ ],
245
+ axis=-1,
246
+ ) # (B, N, PD1+PD2+427)
247
+ atom_feat = self.atom_feat_proj(atom_feat) # (B, N, D)
248
+
249
+ atom_coord = self.pos_embedder(pos=noised_pos) # (B, N, PD1)
250
+ atom_coord = self.atom_pos_proj(atom_coord) # (B, N, D)
251
+
252
+ atom_in = mx.concatenate([atom_feat, atom_coord], axis=-1)
253
+ atom_in = self.atom_in_proj(atom_in) # (B, N, D)
254
+
255
+ # position embeddings for Axial RoPE
256
+ atom_pe_pos = mx.concatenate(
257
+ [
258
+ ref_space_uid[..., None].astype(mx.float32), # (B, N, 1)
259
+ feats["ref_pos"], # (B, N, 3)
260
+ ],
261
+ axis=-1,
262
+ ) # (B, N, 4)
263
+
264
+ token_pe_pos = mx.concatenate(
265
+ [
266
+ feats["residue_index"][..., None].astype(mx.float32), # (B, M, 1)
267
+ feats["entity_id"][..., None].astype(mx.float32), # (B, M, 1)
268
+ feats["asym_id"][..., None].astype(mx.float32), # (B, M, 1)
269
+ feats["sym_id"][..., None].astype(mx.float32), # (B, M, 1)
270
+ ],
271
+ axis=-1,
272
+ ) # (B, M, 4)
273
+
274
+ atom_c_emb_enc = self.atom_enc_cond_proj(c_emb)
275
+ atom_latent = self.context2atom_proj(atom_in)
276
+ atom_latent = self.atom_encoder_transformer(
277
+ latents=atom_latent,
278
+ c=atom_c_emb_enc,
279
+ attention_mask=atom_attn_mask_enc,
280
+ pos=atom_pe_pos,
281
+ )
282
+
283
+
284
+ atom_latent = self.atom2latent_proj(atom_latent)
285
+
286
+ # grouping: aggregate atom tokens to residue tokens
287
+ atom_to_token_mean = atom_to_token / (
288
+ atom_to_token.sum(axis=1, keepdims=True) + 1e-6
289
+ )
290
+ latent = mx.matmul(atom_to_token_mean.swapaxes(axis1=1, axis2=2), atom_latent)
291
+ assert latent.shape[1] == M
292
+
293
+ esm_s = (
294
+ mx.softmax(self.esm_s_combine, axis=0)[None, ...] @ feats["esm_s"]
295
+ ).squeeze(axis=2)
296
+
297
+ # MLX is only interended for inference, we do not drop any ids
298
+ esm_emb = self.esm_s_proj(esm_s, train=False)
299
+ assert esm_emb.shape[1] == latent.shape[1]
300
+
301
+ latent = self.esm_cat_proj(mx.concatenate([latent, esm_emb], axis=-1))
302
+
303
+ # residue trunk
304
+ latent = self.trunk(
305
+ latents=latent,
306
+ c=c_emb,
307
+ attention_mask=None,
308
+ pos=token_pe_pos,
309
+ )
310
+
311
+ # ungrouping: broadcast residue tokens to atom tokens
312
+ output = mx.matmul(atom_to_token, latent)
313
+ assert output.shape[1] == N
314
+
315
+ # add skip connection
316
+ output = output + atom_latent
317
+ output = self.latent2atom_proj(output)
318
+
319
+ # atom decoder
320
+ atom_c_emb_dec = self.atom_dec_cond_proj(c_emb)
321
+ output = self.atom_decoder_transformer(
322
+ latents=output,
323
+ c=atom_c_emb_dec,
324
+ attention_mask=atom_attn_mask_dec,
325
+ pos=atom_pe_pos,
326
+ )
327
+
328
+ output = self.final_layer(output, c=c_emb)
329
+
330
+ return {
331
+ "predict_velocity": output,
332
+ "latent": latent,
333
+ }
models/simplefold/mlx/blocks.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import mlx.nn as nn
7
+ import mlx.core as mx
8
+
9
+ from .simplefold.mlx.layers import modulate, SwiGLUFeedForward
10
+
11
+
12
+ class DiTBlock(nn.Module):
13
+ """
14
+ A DiT block with adaptive layer norm zero (adaLN-Zero) conditioning.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ self_attention_layer,
20
+ hidden_size,
21
+ mlp_ratio=4.0,
22
+ use_swiglu=True,
23
+ ):
24
+ super().__init__()
25
+ self.norm1 = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
26
+ self.attn = self_attention_layer()
27
+ self.norm2 = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
28
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
29
+
30
+ assert use_swiglu, "Need use_swiglu=True for MLX"
31
+ self.mlp = SwiGLUFeedForward(hidden_size, mlp_hidden_dim)
32
+
33
+ self.adaLN_modulation = nn.Sequential(
34
+ nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size, bias=True)
35
+ )
36
+
37
+ def __call__(
38
+ self,
39
+ latents,
40
+ c,
41
+ **kwargs,
42
+ ):
43
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
44
+ self.adaLN_modulation(c).split(6, axis=1)
45
+ )
46
+
47
+ _latents, _ = self.attn(
48
+ modulate(self.norm1(latents), shift_msa, scale_msa), **kwargs
49
+ )
50
+ latents = latents + mx.expand_dims(gate_msa, axis=1) * _latents
51
+
52
+ latents = latents + mx.expand_dims(gate_mlp, axis=1) * self.mlp(
53
+ modulate(self.norm2(latents), shift_mlp, scale_mlp)
54
+ )
55
+ return latents
56
+
57
+
58
+ class TransformerBlock(nn.Module):
59
+ """
60
+ A DiT block with adaptive layer norm zero (adaLN-Zero) conditioning.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ self_attention_layer,
66
+ hidden_size,
67
+ mlp_ratio=4.0,
68
+ use_swiglu=False,
69
+ ):
70
+ super().__init__()
71
+ self.norm1 = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
72
+ self.attn = self_attention_layer()
73
+ self.norm2 = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
74
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
75
+
76
+ assert use_swiglu, "Need use_swiglu=True for MLX"
77
+ self.mlp = SwiGLUFeedForward(hidden_size, mlp_hidden_dim)
78
+
79
+ def __call__(
80
+ self,
81
+ latents,
82
+ **kwargs,
83
+ ):
84
+ _latents, _ = self.attn(self.norm1(latents), **kwargs)
85
+ latents = latents + _latents
86
+ latents = latents + self.mlp(self.norm2(latents))
87
+ return latents
88
+
89
+
90
+ # Homogen trunk, same block applied iteratively
91
+ class HomogenTrunk(nn.Module):
92
+ def __init__(self, block, depth):
93
+ super().__init__()
94
+ self.blocks = [block() for _ in range(depth)]
95
+
96
+ def __call__(self, latents, c, **kwargs):
97
+ for i, block in enumerate(self.blocks):
98
+ kwargs["layer_idx"] = i
99
+ latents = block(latents=latents, c=c, **kwargs)
100
+ return latents
models/simplefold/mlx/confidence_module.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import mlx.nn as nn
7
+ import mlx.core as mx
8
+
9
+
10
+ def compute_aggregated_metric(logits, end=1.0):
11
+ """Compute the metric from the logits.
12
+
13
+ Parameters
14
+ ----------
15
+ logits : torch.Tensor
16
+ The logits of the metric
17
+ end : float
18
+ Max value of the metric, by default 1.0
19
+
20
+ Returns
21
+ -------
22
+ Tensor
23
+ The metric value
24
+
25
+ """
26
+ num_bins = logits.shape[-1]
27
+ bin_width = end / num_bins
28
+ bounds = mx.arange(start=0.5 * bin_width, stop=end, step=bin_width)
29
+ probs = mx.softmax(logits, axis=-1)
30
+ plddt = mx.sum(
31
+ probs * bounds.reshape(*((1,) * len(probs.shape[:-1])), *bounds.shape),
32
+ axis=-1,
33
+ )
34
+ return plddt
35
+
36
+
37
+ class ConfidenceModule(nn.Module):
38
+ def __init__(
39
+ self,
40
+ hidden_size,
41
+ transformer_blocks=None,
42
+ num_plddt_bins=50,
43
+ ):
44
+ super().__init__()
45
+ self.transformer_blocks = transformer_blocks
46
+ self.to_plddt_logits = nn.Sequential(
47
+ nn.Linear(hidden_size, hidden_size),
48
+ nn.LayerNorm(hidden_size),
49
+ nn.SiLU(),
50
+ nn.Linear(hidden_size, num_plddt_bins),
51
+ )
52
+
53
+ def __call__(
54
+ self,
55
+ latent,
56
+ feats,
57
+ ):
58
+ if self.transformer_blocks is not None:
59
+ token_pe_pos = mx.concatenate(
60
+ [
61
+ feats["residue_index"][..., None].astype(mx.float32), # (B, M, 1)
62
+ feats["entity_id"][..., None].astype(mx.float32), # (B, M, 1)
63
+ feats["asym_id"][..., None].astype(mx.float32), # (B, M, 1)
64
+ feats["sym_id"][..., None].astype(mx.float32), # (B, M, 1)
65
+ ],
66
+ axis=-1,
67
+ )
68
+
69
+ latent = self.transformer_blocks(
70
+ latents=latent,
71
+ c=None,
72
+ pos=token_pe_pos,
73
+ )
74
+
75
+ # Compute the pLDDT
76
+ plddt_logits = self.to_plddt_logits(latent)
77
+
78
+ # Compute the aggregated pLDDT
79
+ plddt = compute_aggregated_metric(plddt_logits)
80
+
81
+ return dict(
82
+ plddt=plddt,
83
+ plddt_logits=plddt_logits,
84
+ )
models/simplefold/mlx/esm_modules.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ # Started from https://github.com/facebookresearch/esm/tree/main,
7
+ # licensed under MIT License, Copyright (c) Meta Platforms, Inc. and affiliates.
8
+
9
+ from typing import Optional
10
+
11
+ import mlx.core as mx
12
+ import mlx.nn as nn
13
+
14
+ from mlx.nn import LayerNorm as ESM1bLayerNorm
15
+ from mlx.nn import gelu
16
+ from .simplefold.mlx.esm_multihead_attention import MultiheadAttention # noqa
17
+
18
+
19
+ def symmetrize(x):
20
+ "Make layer symmetric in final two dimensions, used for contact prediction."
21
+ return x + mx.swapaxes(x, axis1=-1, axis2=-2)
22
+
23
+
24
+ def apc(x):
25
+ "Perform average product correct, used for contact prediction."
26
+ a1 = x.sum(-1, keepdims=True)
27
+ a2 = x.sum(-2, keepdims=True)
28
+ a12 = x.sum((-1, -2), keepdims=True)
29
+
30
+ avg = a1 * a2
31
+ avg = mx.divide(avg, a12)
32
+ normalized = x - avg
33
+ return normalized
34
+
35
+
36
+ class ESM1LayerNorm(nn.Module):
37
+ def __init__(self, hidden_size, eps=1e-12, affine=True):
38
+ """Construct a layernorm layer in the TF style (eps inside the sqrt)."""
39
+ super().__init__()
40
+ self.hidden_size = (
41
+ (hidden_size,) if isinstance(hidden_size, int) else tuple(hidden_size)
42
+ )
43
+ self.eps = eps
44
+ self.affine = bool(affine)
45
+ if self.affine:
46
+ self.weight = mx.array(mx.ones(hidden_size))
47
+ self.bias = mx.array(mx.zeros(hidden_size))
48
+ else:
49
+ self.weight, self.bias = None, None
50
+
51
+ def __call__(self, x):
52
+ dims = tuple(-(i + 1) for i in range(len(self.hidden_size)))
53
+ means = x.mean(dims, keepdims=True)
54
+ x_zeromean = x - means
55
+ variances = x_zeromean.pow(2).mean(dims, keepdims=True)
56
+ x = x_zeromean / mx.sqrt(variances + self.eps)
57
+ if self.affine:
58
+ x = (self.weight * x) + self.bias
59
+ return x
60
+
61
+
62
+ class TransformerLayer(nn.Module):
63
+
64
+ def __init__(
65
+ self,
66
+ embed_dim,
67
+ ffn_embed_dim,
68
+ attention_heads,
69
+ add_bias_kv=True,
70
+ use_esm1b_layer_norm=False, # This is true in the implementation
71
+ use_rotary_embeddings: bool = False, # This is true in the implementation
72
+ ):
73
+ super().__init__()
74
+ self.embed_dim = embed_dim
75
+ self.ffn_embed_dim = ffn_embed_dim
76
+ self.attention_heads = attention_heads
77
+ self.use_rotary_embeddings = use_rotary_embeddings
78
+ self._init_submodules(add_bias_kv, use_esm1b_layer_norm)
79
+
80
+ def _init_submodules(self, add_bias_kv, use_esm1b_layer_norm):
81
+ BertLayerNorm = ESM1bLayerNorm if use_esm1b_layer_norm else ESM1LayerNorm
82
+
83
+ self.self_attn = MultiheadAttention(
84
+ self.embed_dim,
85
+ self.attention_heads,
86
+ add_bias_kv=add_bias_kv,
87
+ add_zero_attn=False,
88
+ use_rotary_embeddings=self.use_rotary_embeddings,
89
+ )
90
+ self.self_attn_layer_norm = BertLayerNorm(self.embed_dim)
91
+
92
+ self.fc1 = nn.Linear(self.embed_dim, self.ffn_embed_dim)
93
+ self.fc2 = nn.Linear(self.ffn_embed_dim, self.embed_dim)
94
+
95
+ self.final_layer_norm = BertLayerNorm(self.embed_dim)
96
+
97
+ def __call__(
98
+ self,
99
+ x,
100
+ self_attn_mask=None,
101
+ self_attn_padding_mask=None,
102
+ need_head_weights=False,
103
+ ):
104
+ residual = x
105
+ x = self.self_attn_layer_norm(x)
106
+ x, attn = self.self_attn(
107
+ query=x,
108
+ key=x,
109
+ value=x,
110
+ key_padding_mask=self_attn_padding_mask,
111
+ need_weights=True,
112
+ need_head_weights=need_head_weights,
113
+ attn_mask=self_attn_mask,
114
+ )
115
+ x = residual + x
116
+
117
+ residual = x
118
+ x = self.final_layer_norm(x)
119
+ x = gelu(self.fc1(x))
120
+ x = self.fc2(x)
121
+ x = residual + x
122
+
123
+ return x, attn
124
+
125
+
126
+ class RobertaLMHead(nn.Module):
127
+ """Head for masked language modeling."""
128
+
129
+ def __init__(self, embed_dim, output_dim, weight):
130
+ super().__init__()
131
+ self.dense = nn.Linear(embed_dim, embed_dim)
132
+ self.layer_norm = ESM1bLayerNorm(embed_dim)
133
+ self.weight = weight
134
+ self.bias = mx.array(mx.zeros(output_dim))
135
+
136
+ def __call__(self, features):
137
+ x = self.dense(features)
138
+ x = gelu(x)
139
+ x = self.layer_norm(x)
140
+ # project back to size of vocabulary with bias
141
+ x = mx.matmul(x, mx.swapaxes(self.weight, axis1=0, axis2=1)) + self.bias
142
+ return x
143
+
144
+
145
+ class ContactPredictionHead(nn.Module):
146
+ """Performs symmetrization, apc, and computes a logistic regression on the output features"""
147
+
148
+ def __init__(
149
+ self,
150
+ in_features: int,
151
+ prepend_bos: bool,
152
+ append_eos: bool,
153
+ bias=True,
154
+ eos_idx: Optional[int] = None,
155
+ ):
156
+ super().__init__()
157
+ self.in_features = in_features
158
+ self.prepend_bos = prepend_bos
159
+ self.append_eos = append_eos
160
+ if append_eos and eos_idx is None:
161
+ raise ValueError(
162
+ "Using an alphabet with eos token, but no eos token was passed in."
163
+ )
164
+ self.eos_idx = eos_idx
165
+ self.regression = nn.Linear(in_features, 1, bias)
166
+ self.activation = nn.Sigmoid()
167
+
168
+ def __call__(self, tokens, attentions):
169
+ # remove eos token attentions
170
+ if self.append_eos:
171
+ eos_mask = tokens.ne(self.eos_idx)
172
+ eos_mask = eos_mask[:, None, ...] * eos_mask[:, :, None, ...]
173
+ attentions = attentions * eos_mask[:, None, None, :, :]
174
+ attentions = attentions[..., :-1, :-1]
175
+ # remove cls token attentions
176
+ if self.prepend_bos:
177
+ attentions = attentions[..., 1:, 1:]
178
+ batch_size, layers, heads, seqlen, _ = attentions.shape
179
+ attentions = attentions.reshape(batch_size, layers * heads, seqlen, seqlen)
180
+
181
+ # features: B x C x T x T
182
+ attentions = apc(symmetrize(attentions))
183
+ attentions = attentions.transpose(0, 2, 3, 1)
184
+ return self.activation(self.regression(attentions).squeeze(3))
models/simplefold/mlx/esm_multihead_attention.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ # Started from https://github.com/facebookresearch/esm/tree/main,
7
+ # licensed under MIT License, Copyright (c) Meta Platforms, Inc. and affiliates.
8
+
9
+ import mlx.nn as nn
10
+ import mlx.core as mx
11
+ from .simplefold.mlx.esm_rotary_embedding import RotaryEmbedding
12
+
13
+
14
+ def utils_softmax(x, dim: int, onnx_trace: bool = False):
15
+ return mx.softmax(x.astype(mx.float32), axis=dim)
16
+
17
+
18
+ def masked_fill_mlx(x, mask, value):
19
+ return mx.where(mask, value, x)
20
+
21
+
22
+ class MultiheadAttention(nn.Module):
23
+ """Multi-headed attention.
24
+
25
+ See "Attention Is All You Need" for more details.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ embed_dim,
31
+ num_heads,
32
+ kdim=None,
33
+ vdim=None,
34
+ dropout=0.0,
35
+ bias=True,
36
+ add_bias_kv: bool = False,
37
+ add_zero_attn: bool = False,
38
+ self_attention: bool = False,
39
+ encoder_decoder_attention: bool = False,
40
+ use_rotary_embeddings: bool = False,
41
+ ):
42
+ super().__init__()
43
+ self.embed_dim = embed_dim
44
+ self.kdim = kdim if kdim is not None else embed_dim
45
+ self.vdim = vdim if vdim is not None else embed_dim
46
+ self.qkv_same_dim = self.kdim == embed_dim and self.vdim == embed_dim
47
+
48
+ self.num_heads = num_heads
49
+ self.dropout = dropout
50
+ self.head_dim = embed_dim // num_heads
51
+ assert (
52
+ self.head_dim * num_heads == self.embed_dim
53
+ ), "embed_dim must be divisible by num_heads"
54
+ self.scaling = self.head_dim**-0.5
55
+
56
+ self.self_attention = self_attention
57
+
58
+ self.k_proj = nn.Linear(self.kdim, embed_dim, bias=bias)
59
+ self.v_proj = nn.Linear(self.vdim, embed_dim, bias=bias)
60
+ self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
61
+
62
+ self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
63
+
64
+ if add_bias_kv:
65
+ self.bias_k = mx.array(1, 1, embed_dim)
66
+ self.bias_v = mx.array(1, 1, embed_dim)
67
+ else:
68
+ self.bias_k = self.bias_v = None
69
+
70
+ self.add_zero_attn = add_zero_attn
71
+
72
+ self.rot_emb = RotaryEmbedding(dim=self.head_dim)
73
+
74
+ self.enable_torch_version = False
75
+
76
+ def __call__(
77
+ self,
78
+ query,
79
+ key,
80
+ value,
81
+ key_padding_mask=None,
82
+ incremental_state=None,
83
+ need_weights=True,
84
+ static_kv=False,
85
+ attn_mask=None,
86
+ before_softmax=False,
87
+ need_head_weights=False,
88
+ ):
89
+ """Input shape: Time x Batch x Channel
90
+
91
+ Args:
92
+ key_padding_mask (ByteTensor, optional): mask to exclude
93
+ keys that are pads, of shape `(batch, src_len)`, where
94
+ padding elements are indicated by 1s.
95
+ need_weights (bool, optional): return the attention weights,
96
+ averaged over heads (default: False).
97
+ attn_mask (ByteTensor, optional): typically used to
98
+ implement causal attention, where the mask prevents the
99
+ attention from looking forward in time (default: None).
100
+ before_softmax (bool, optional): return the raw attention
101
+ weights and values before the attention softmax.
102
+ need_head_weights (bool, optional): return the attention
103
+ weights for each head. Implies *need_weights*. Default:
104
+ return the average attention weights over all heads.
105
+ """
106
+
107
+ tgt_len, bsz, embed_dim = query.shape
108
+ assert embed_dim == self.embed_dim
109
+ assert list(query.shape) == [tgt_len, bsz, embed_dim]
110
+
111
+ if self.self_attention:
112
+ q = self.q_proj(query)
113
+ k = self.k_proj(query)
114
+ v = self.v_proj(query)
115
+ else:
116
+ assert key is not None and value is not None
117
+ q = self.q_proj(query)
118
+ k = self.k_proj(key)
119
+ v = self.v_proj(value)
120
+ q *= self.scaling
121
+
122
+ if self.bias_k is not None:
123
+ assert self.bias_v is not None
124
+
125
+ # TODO: mlx not support array repeat or new_zeros
126
+ k = mx.concatenate([k, mx.tile(self.bias_k, (1, bsz, 1))])
127
+ v = mx.concatenate([v, mx.tile(self.bias_v, (1, bsz, 1))])
128
+ if attn_mask is not None:
129
+ attn_mask = mx.concatenate(
130
+ [
131
+ attn_mask,
132
+ mx.zeros((attn_mask.shape[0], 1), dtype=attn_mask.dtype),
133
+ ],
134
+ axis=1,
135
+ )
136
+ if key_padding_mask is not None:
137
+ key_padding_mask = mx.concatenate(
138
+ [
139
+ key_padding_mask,
140
+ mx.zeros(
141
+ (key_padding_mask.shape[0], 1), dtype=key_padding_mask.dtype
142
+ ),
143
+ ],
144
+ axis=1,
145
+ )
146
+
147
+ q = mx.swapaxes(
148
+ mx.contiguous(q).reshape(tgt_len, bsz * self.num_heads, self.head_dim),
149
+ axis1=0,
150
+ axis2=1,
151
+ )
152
+
153
+ if k is not None:
154
+ k = mx.swapaxes(
155
+ mx.contiguous(k).reshape(-1, bsz * self.num_heads, self.head_dim),
156
+ axis1=0,
157
+ axis2=1,
158
+ )
159
+
160
+ if v is not None:
161
+ v = mx.swapaxes(
162
+ mx.contiguous(v).reshape(-1, bsz * self.num_heads, self.head_dim),
163
+ axis1=0,
164
+ axis2=1,
165
+ )
166
+
167
+ assert k is not None
168
+ src_len = k.shape[1]
169
+
170
+ # This is part of a workaround to get around fork/join parallelism
171
+ # not supporting Optional types.
172
+
173
+ if key_padding_mask is not None and key_padding_mask.ndim == 0:
174
+ key_padding_mask = None
175
+
176
+ if key_padding_mask is not None:
177
+ assert key_padding_mask.shape[0] == bsz
178
+ assert key_padding_mask.shape[1] == src_len
179
+
180
+ if self.rot_emb:
181
+
182
+ q, k = self.rot_emb(q, k)
183
+
184
+ attn_weights = mx.matmul(q, mx.swapaxes(k, axis1=1, axis2=2))
185
+ attn_weights = MultiheadAttention.apply_sparse_mask(
186
+ attn_weights, tgt_len, src_len, bsz
187
+ )
188
+
189
+ assert list(attn_weights.shape) == [bsz * self.num_heads, tgt_len, src_len]
190
+
191
+ if attn_mask is not None:
192
+ attn_mask = attn_mask[None, ...]
193
+ attn_weights += attn_mask
194
+
195
+ if key_padding_mask is not None:
196
+ # don't attend to padding symbols
197
+ attn_weights = attn_weights.reshape(bsz, self.num_heads, tgt_len, src_len)
198
+ attn_weights = masked_fill_mlx(
199
+ attn_weights,
200
+ (key_padding_mask[:, None, None, ...] == 1.0),
201
+ float("-inf"),
202
+ )
203
+ attn_weights = attn_weights.reshape(bsz * self.num_heads, tgt_len, src_len)
204
+
205
+ attn_weights_float = utils_softmax(attn_weights, dim=-1, onnx_trace=False)
206
+ attn_weights = attn_weights_float.astype(attn_weights.dtype)
207
+
208
+ attn_probs = attn_weights.astype(attn_weights.dtype)
209
+ assert v is not None
210
+ attn = mx.matmul(attn_probs, v)
211
+
212
+ assert list(attn.shape) == [bsz * self.num_heads, tgt_len, self.head_dim]
213
+
214
+ attn = mx.contiguous(mx.swapaxes(attn, axis1=0, axis2=1)).reshape(
215
+ tgt_len, bsz, embed_dim
216
+ )
217
+ attn = self.out_proj(attn)
218
+
219
+ attn_weights = None
220
+
221
+ return attn, attn_weights
222
+
223
+ def apply_sparse_mask(attn_weights, tgt_len: int, src_len: int, bsz: int):
224
+ return attn_weights
models/simplefold/mlx/esm_network.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ # Started from https://github.com/facebookresearch/esm/tree/main,
7
+ # licensed under MIT License, Copyright (c) Meta Platforms, Inc. and affiliates.
8
+
9
+ from typing import Union
10
+ import mlx.core as mx
11
+ import mlx.nn as nn
12
+
13
+ from .simplefold.mlx.esm_modules import (
14
+ ContactPredictionHead,
15
+ ESM1bLayerNorm,
16
+ RobertaLMHead,
17
+ TransformerLayer,
18
+ )
19
+ from onescience.datapipes.esm import Alphabet
20
+
21
+
22
+ def masked_fill_mlx(x, mask, value):
23
+ return mx.where(mask, value, x)
24
+
25
+
26
+ class ESM2(nn.Module):
27
+ def __init__(
28
+ self,
29
+ num_layers: int = 33,
30
+ embed_dim: int = 1280,
31
+ attention_heads: int = 20,
32
+ alphabet: Union[Alphabet, str] = "ESM-1b",
33
+ token_dropout: bool = True,
34
+ ):
35
+ super().__init__()
36
+ self.num_layers = num_layers
37
+ self.embed_dim = embed_dim
38
+ self.attention_heads = attention_heads
39
+ if not isinstance(alphabet, Alphabet):
40
+ alphabet = Alphabet.from_architecture(alphabet)
41
+ self.alphabet = alphabet
42
+ self.alphabet_size = len(alphabet)
43
+ self.padding_idx = alphabet.padding_idx
44
+ self.mask_idx = alphabet.mask_idx
45
+ self.cls_idx = alphabet.cls_idx
46
+ self.eos_idx = alphabet.eos_idx
47
+ self.prepend_bos = alphabet.prepend_bos
48
+ self.append_eos = alphabet.append_eos
49
+ self.token_dropout = token_dropout
50
+
51
+ self._init_submodules()
52
+
53
+ def _init_submodules(self):
54
+ self.embed_scale = 1
55
+ self.embed_tokens = mx.zeros((self.alphabet_size, self.embed_dim))
56
+
57
+ self.layers = [
58
+ TransformerLayer(
59
+ self.embed_dim,
60
+ 4 * self.embed_dim,
61
+ self.attention_heads,
62
+ add_bias_kv=False,
63
+ use_esm1b_layer_norm=True,
64
+ use_rotary_embeddings=True,
65
+ )
66
+ for _ in range(self.num_layers)
67
+ ]
68
+
69
+ self.contact_head = ContactPredictionHead(
70
+ self.num_layers * self.attention_heads,
71
+ self.prepend_bos,
72
+ self.append_eos,
73
+ eos_idx=self.eos_idx,
74
+ )
75
+ self.emb_layer_norm_after = ESM1bLayerNorm(self.embed_dim)
76
+
77
+ self.lm_head = RobertaLMHead(
78
+ embed_dim=self.embed_dim,
79
+ output_dim=self.alphabet_size,
80
+ weight=self.embed_tokens,
81
+ )
82
+
83
+ def __call__(
84
+ self, tokens, repr_layers=[], need_head_weights=False, return_contacts=False
85
+ ):
86
+ if return_contacts:
87
+ need_head_weights = True
88
+
89
+ assert tokens.ndim == 2
90
+ padding_mask = mx.equal(tokens, self.padding_idx) # B, T
91
+
92
+ x = self.embed_scale * self.embed_tokens[tokens, :]
93
+
94
+ if self.token_dropout:
95
+ x = masked_fill_mlx(x, (tokens == self.mask_idx)[..., None], 0.0)
96
+ # x: B x T x C
97
+ mask_ratio_train = 0.15 * 0.8
98
+ src_lengths = (~padding_mask).sum(axis=-1)
99
+ mask_ratio_observed = (tokens == self.mask_idx).sum(axis=-1).astype(
100
+ x.dtype
101
+ ) / src_lengths
102
+ x = x * (1 - mask_ratio_train) / (1 - mask_ratio_observed)[:, None, None]
103
+
104
+ if padding_mask is not None:
105
+ x = x * (1 - padding_mask[..., None].astype(x.dtype))
106
+
107
+ repr_layers = set(repr_layers)
108
+ hidden_representations = {}
109
+ if 0 in repr_layers:
110
+ hidden_representations[0] = x
111
+
112
+ if need_head_weights:
113
+ attn_weights = []
114
+
115
+ # (B, T, E) => (T, B, E)
116
+ x = mx.swapaxes(x, axis1=0, axis2=1)
117
+
118
+ if not padding_mask.any():
119
+ padding_mask = None
120
+
121
+ for layer_idx, layer in enumerate(self.layers):
122
+ x, attn = layer(
123
+ x,
124
+ self_attn_padding_mask=padding_mask,
125
+ need_head_weights=need_head_weights,
126
+ )
127
+
128
+ if (layer_idx + 1) in repr_layers:
129
+ hidden_representations[layer_idx + 1] = mx.swapaxes(x, axis1=0, axis2=1)
130
+ if need_head_weights:
131
+ # (H, B, T, T) => (B, H, T, T)
132
+ attn_weights.append(mx.swapaxes(attn, axis1=1, axis2=0))
133
+
134
+ x = self.emb_layer_norm_after(x)
135
+ x = mx.swapaxes(x, axis1=0, axis2=1) # (T, B, E) => (B, T, E)
136
+
137
+ # last hidden representation should have layer norm applied
138
+ if (layer_idx + 1) in repr_layers:
139
+ hidden_representations[layer_idx + 1] = x
140
+ x = self.lm_head(x)
141
+
142
+ result = {"logits": x, "representations": hidden_representations}
143
+ if need_head_weights:
144
+ # attentions: B x L x H x T x T
145
+ attentions = mx.stack(attn_weights, axis=1)
146
+ if padding_mask is not None:
147
+ attention_mask = 1 - padding_mask.astype(attentions.dtype)
148
+ attention_mask = (
149
+ attention_mask[:, None, ...] * attention_mask[:, :, None, ...]
150
+ )
151
+ attentions = attentions * attention_mask[:, None, None, :, :]
152
+ result["attentions"] = attentions
153
+ if return_contacts:
154
+ contacts = self.contact_head(tokens, attentions)
155
+ result["contacts"] = contacts
156
+
157
+ return result
158
+
159
+ def predict_contacts(self, tokens):
160
+ return self(tokens, return_contacts=True)["contacts"]
models/simplefold/mlx/esm_rotary_embedding.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ # Started from https://github.com/facebookresearch/esm/tree/main,
7
+ # licensed under MIT License, Copyright (c) Meta Platforms, Inc. and affiliates.
8
+
9
+ import mlx.core as mx
10
+ import mlx.nn as nn
11
+
12
+
13
+ def rotate_half(x):
14
+ x1, x2 = x.split(2, axis=-1)
15
+ return mx.concatenate([-x2, x1], axis=-1)
16
+
17
+
18
+ def apply_rotary_pos_emb(x, cos, sin):
19
+ cos = cos[:, : x.shape[-2], :]
20
+ sin = sin[:, : x.shape[-2], :]
21
+
22
+ return (x * cos) + (rotate_half(x) * sin)
23
+
24
+
25
+ class RotaryEmbedding(nn.Module):
26
+ """
27
+ The rotary position embeddings from RoFormer_ (Su et. al).
28
+ A crucial insight from the method is that the query and keys are
29
+ transformed by rotation matrices which depend on the relative positions.
30
+ Other implementations are available in the Rotary Transformer repo_ and in
31
+ GPT-NeoX_, GPT-NeoX was an inspiration
32
+ .. _RoFormer: https://arxiv.org/abs/2104.09864
33
+ .. _repo: https://github.com/ZhuiyiTechnology/roformer
34
+ .. _GPT-NeoX: https://github.com/EleutherAI/gpt-neox
35
+ .. warning: Please note that this embedding is not registered on purpose, as it is transformative
36
+ (it does not create the embedding dimension) and will likely be picked up (imported) on a ad-hoc basis
37
+ """
38
+
39
+ def __init__(self, dim: int, *_, **__):
40
+ super().__init__()
41
+ # Generate and save the inverse frequency buffer (non trainable)
42
+ self.inv_freq = mx.array(
43
+ 1.0 / (10000 ** (mx.arange(0, dim, 2).astype(mx.float32) / dim))
44
+ )
45
+
46
+ self._seq_len_cached = None
47
+ self._cos_cached = None
48
+ self._sin_cached = None
49
+
50
+ def _update_cos_sin_tables(self, x, seq_dimension=1):
51
+ seq_len = x.shape[seq_dimension]
52
+
53
+ # Reset the tables if the sequence length has changed,
54
+ # or if we're on a new device (possibly due to tracing for instance)
55
+ if seq_len != self._seq_len_cached:
56
+ self._seq_len_cached = seq_len
57
+ t = mx.arange(x.shape[seq_dimension]).astype(self.inv_freq.dtype)
58
+ freqs = mx.einsum("i,j->ij", t, self.inv_freq)
59
+ emb = mx.concatenate([freqs, freqs], axis=-1)
60
+
61
+ self._cos_cached = emb.cos()[None, :, :]
62
+ self._sin_cached = emb.sin()[None, :, :]
63
+
64
+ return self._cos_cached, self._sin_cached
65
+
66
+ def __call__(self, q, k):
67
+ self._cos_cached, self._sin_cached = self._update_cos_sin_tables(
68
+ k, seq_dimension=-2
69
+ )
70
+
71
+ return (
72
+ apply_rotary_pos_emb(q, self._cos_cached, self._sin_cached),
73
+ apply_rotary_pos_emb(k, self._cos_cached, self._sin_cached),
74
+ )
models/simplefold/mlx/layers.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import math
7
+ from einops.array_api import rearrange
8
+ from operator import __add__
9
+ import mlx.core as mx
10
+ import mlx.nn as nn
11
+
12
+
13
+ def modulate(x, shift, scale):
14
+ return x * (1 + mx.expand_dims(scale, axis=1)) + mx.expand_dims(shift, axis=1)
15
+
16
+
17
+ #################################################################################
18
+ # Attention Layers #
19
+ #################################################################################
20
+
21
+
22
+ class SelfAttentionLayer(nn.Module):
23
+ def __init__(
24
+ self,
25
+ hidden_size,
26
+ num_heads=8,
27
+ qkv_bias=False,
28
+ qk_scale=None,
29
+ attn_drop=0.0,
30
+ proj_drop=0.0,
31
+ use_bias=True,
32
+ qk_norm=True,
33
+ pos_embedder=None,
34
+ linear_target: nn.Module = nn.Linear,
35
+ ):
36
+ super().__init__()
37
+ self.num_heads = num_heads
38
+ head_dim = hidden_size // num_heads
39
+ # NOTE scale factor was wrong in my original version,
40
+ # can set manually to be compat with prev weights
41
+ self.scale = qk_scale or head_dim**-0.5
42
+
43
+ self.qkv = linear_target(hidden_size, hidden_size * 3, bias=qkv_bias)
44
+ self.attn_drop = nn.Dropout(attn_drop)
45
+ self.proj = linear_target(hidden_size, hidden_size, bias=use_bias)
46
+ self.proj_drop = nn.Dropout(proj_drop)
47
+
48
+ self.q_norm = nn.RMSNorm(head_dim, eps=1e-8) if qk_norm else nn.Identity()
49
+ self.k_norm = nn.RMSNorm(head_dim, eps=1e-8) if qk_norm else nn.Identity()
50
+
51
+ self.pos_embedder = pos_embedder
52
+
53
+ def __call__(self, x, **kwargs):
54
+ B, N, C = x.shape
55
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
56
+ pos = kwargs.get("pos")
57
+
58
+ qkv = rearrange(qkv, "b n t h c -> t b h n c")
59
+ q, k, v = (
60
+ qkv[0],
61
+ qkv[1],
62
+ qkv[2],
63
+ )
64
+
65
+ q, k = self.q_norm(q), self.k_norm(k)
66
+
67
+ if self.pos_embedder and pos is not None:
68
+ q, k = self.pos_embedder(q, k, pos)
69
+
70
+ attn = (q @ k.swapaxes(axis1=-2, axis2=-1)) * self.scale
71
+ attn = mx.softmax(attn, axis=-1)
72
+ attn = self.attn_drop(attn)
73
+
74
+ x = (attn @ v).swapaxes(axis1=1, axis2=2).reshape(B, N, C)
75
+ x = self.proj(x)
76
+ x = self.proj_drop(x)
77
+ return x
78
+
79
+
80
+ class EfficientSelfAttentionLayer(SelfAttentionLayer):
81
+ """Adapted from https://github.com/facebookresearch/dinov2/blob/main/dinov2/layers/attention.py"""
82
+
83
+ def __init__(
84
+ self,
85
+ *args,
86
+ **kwargs,
87
+ ):
88
+ super().__init__(*args, **kwargs)
89
+
90
+ def __call__(self, x, **kwargs):
91
+ B, N, C = x.shape
92
+ attn_mask = kwargs.get("attention_mask")
93
+ pos = kwargs.get("pos")
94
+
95
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
96
+ qkv = rearrange(qkv, "b n t h c -> t b h n c")
97
+
98
+ q, k, v = (
99
+ qkv[0],
100
+ qkv[1],
101
+ qkv[2],
102
+ )
103
+ if attn_mask is not None:
104
+ attn_mask = attn_mask.astype(q.dtype)
105
+
106
+ # if self.pos_embedder and pos is not None:
107
+ q, k = self.pos_embedder(q, k, pos)
108
+
109
+ q, k = self.q_norm(q), self.k_norm(k)
110
+
111
+ x = mx.fast.scaled_dot_product_attention(
112
+ q, k, v, mask=attn_mask, scale=1.0 / mx.sqrt(q.shape[-1])
113
+ )
114
+
115
+ x = x.swapaxes(axis1=1, axis2=2).reshape(B, N, C)
116
+ x = self.proj(x)
117
+ x = self.proj_drop(x)
118
+
119
+ return_attn = kwargs.get("return_attn", False)
120
+ if return_attn:
121
+ attn = (q @ k.swapaxes(axis1=-2, axis2=-1)) * self.scale
122
+ attn = attn.softmax(axis=-1)
123
+ return x, attn
124
+
125
+ return x, None
126
+
127
+
128
+ def exists(val) -> bool:
129
+ """returns whether val is not none"""
130
+ return val is not None
131
+
132
+
133
+ def default(x, y):
134
+ """returns x if it exists, otherwise y"""
135
+ return x if exists(x) else y
136
+
137
+
138
+ #################################################################################
139
+ # FeedForward Layer #
140
+ #################################################################################
141
+
142
+
143
+ class SwiGLUFeedForward(nn.Module):
144
+ def __init__(self, dim, hidden_dim, multiple_of=256):
145
+ super().__init__()
146
+ hidden_dim = int(2 * hidden_dim / 3)
147
+ hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
148
+
149
+ self.w1 = nn.Linear(dim, hidden_dim, bias=False)
150
+ self.w2 = nn.Linear(hidden_dim, dim, bias=True)
151
+ self.w3 = nn.Linear(dim, hidden_dim, bias=False)
152
+
153
+ def __call__(self, x):
154
+ return self.w2(nn.silu(self.w1(x)) * self.w3(x))
155
+
156
+
157
+ #################################################################################
158
+ # Utility Layers #
159
+ #################################################################################
160
+
161
+
162
+ class TimestepEmbedder(nn.Module):
163
+ """
164
+ Embeds scalar timesteps into vector representations.
165
+ """
166
+
167
+ def __init__(self, hidden_size, frequency_embedding_size=256):
168
+ super().__init__()
169
+ self.mlp = nn.Sequential(
170
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
171
+ nn.SiLU(),
172
+ nn.Linear(hidden_size, hidden_size, bias=True),
173
+ )
174
+ self.frequency_embedding_size = frequency_embedding_size
175
+ self.initialize_weights()
176
+
177
+ def initialize_weights(self):
178
+ nn.init.normal(self.mlp.layers[0].weight, std=0.02)
179
+ nn.init.normal(self.mlp.layers[2].weight, std=0.02)
180
+
181
+ @staticmethod
182
+ def timestep_embedding(t, dim, max_period=10000):
183
+ """
184
+ Create sinusoidal timestep embeddings.
185
+ :param t: a 1-D Tensor of N indices, one per batch element.
186
+ These may be fractional.
187
+ :param dim: the dimension of the output.
188
+ :param max_period: controls the minimum frequency of the embeddings.
189
+ :return: an (N, D) Tensor of positional embeddings.
190
+ """
191
+ # https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
192
+ half = dim // 2
193
+ freqs = mx.exp(
194
+ -math.log(max_period)
195
+ * mx.arange(start=0, stop=half, dtype=mx.float32)
196
+ / half
197
+ )
198
+ args = t[:, None].astype(mx.float32) * freqs[None]
199
+ embedding = mx.concatenate([mx.cos(args), mx.sin(args)], axis=-1)
200
+ if dim % 2:
201
+ embedding = mx.concatenate(
202
+ [embedding, mx.zeros_like(embedding[:, :1])], axis=-1
203
+ )
204
+ return embedding
205
+
206
+ def __call__(self, t):
207
+ t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
208
+ t_emb = self.mlp(t_freq)
209
+ return t_emb
210
+
211
+
212
+ class ConditionEmbedder(nn.Module):
213
+ """
214
+ Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance.
215
+ """
216
+ def __init__(self, input_dim, hidden_size, dropout_prob):
217
+ super().__init__()
218
+ self.proj = nn.Sequential(
219
+ nn.Linear(input_dim, hidden_size),
220
+ nn.LayerNorm(hidden_size),
221
+ nn.SiLU(),
222
+ )
223
+ self.dropout_prob = dropout_prob
224
+ self.null_token = mx.zeros(input_dim)
225
+
226
+ def token_drop(self, cond, force_drop_ids=None):
227
+ """
228
+ cond: (B, N, D)
229
+ Drops conditions to enable classifier-free guidance.
230
+ """
231
+ if force_drop_ids is None:
232
+ drop_ids = mx.random.uniform(cond.shape[0]) < self.dropout_prob
233
+ else:
234
+ drop_ids = force_drop_ids
235
+ cond[drop_ids] = self.null_token[None, None, :]
236
+ return cond
237
+
238
+ def __call__(self, cond, train, force_drop_ids=None):
239
+ use_dropout = self.dropout_prob > 0
240
+ if (train and use_dropout) or (force_drop_ids is not None):
241
+ cond = self.token_drop(cond, force_drop_ids)
242
+ embeddings = self.proj(cond)
243
+ return embeddings
244
+
245
+
246
+ class FinalLayer(nn.Module):
247
+ """
248
+ The final layer of DiT.
249
+ """
250
+
251
+ def __init__(self, hidden_size, out_channels, c_dim=None):
252
+ super().__init__()
253
+ self.norm_final = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
254
+ self.linear = nn.Linear(hidden_size, out_channels, bias=True)
255
+ self.adaLN_modulation = nn.Sequential(
256
+ nn.SiLU(), nn.Linear(c_dim, 2 * hidden_size, bias=True)
257
+ )
258
+
259
+ def __call__(self, x, c):
260
+ shift, scale = self.adaLN_modulation(c).split(2, axis=1)
261
+ x = modulate(self.norm_final(x), shift, scale)
262
+ x = self.linear(x)
263
+ return x
models/simplefold/mlx/pos_embed.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import mlx.core as mx
7
+ import mlx.nn as nn
8
+ import math
9
+ from einops.array_api import rearrange
10
+ import torch
11
+ import numpy as np
12
+
13
+
14
+ class AbsolutePositionEncoding(nn.Module):
15
+ def __init__(self, in_dim, embed_dim, include_input=False):
16
+ super().__init__()
17
+ self.in_dim = in_dim
18
+ self.hidden_dim = embed_dim
19
+ self.include_input = include_input
20
+ assert embed_dim % in_dim == 0, "embed_dim must be divisible by in_dim"
21
+ self.embed_dim = embed_dim + in_dim if include_input else embed_dim
22
+
23
+ def __call__(self, pos):
24
+ pos_embs = []
25
+ for i in range(self.in_dim):
26
+ pe = self.get_1d_pos_embed(pos[..., i])
27
+ pos_embs.append(pe)
28
+ if self.include_input:
29
+ pos_embs.append(pos)
30
+ pos_embs = mx.concatenate(pos_embs, axis=-1)
31
+ return pos_embs
32
+
33
+ def get_1d_pos_embed(self, pos):
34
+ """
35
+ https://github.com/facebookresearch/DiT/blob/main/models.py#L303
36
+ """
37
+ embed_dim = self.hidden_dim // (self.in_dim * 2)
38
+ omega = 2 ** mx.linspace(0, math.log(224, 2) - 1, embed_dim).astype(mx.float32)
39
+ omega *= math.pi
40
+
41
+ if len(pos.shape) == 1:
42
+ out = mx.einsum("m,d->md", pos, omega) # (M, D/2), outer product
43
+ elif len(pos.shape) == 2:
44
+ out = mx.einsum("nm,d->nmd", pos, omega)
45
+
46
+ emb_sin = mx.sin(out)
47
+ emb_cos = mx.cos(out) # (*, M, D/2)
48
+ emb = mx.concatenate([emb_sin, emb_cos], axis=-1) # (*, M, D)
49
+ return emb
50
+
51
+
52
+ class FourierPositionEncoding(nn.Module):
53
+ def __init__(
54
+ self,
55
+ in_dim: int,
56
+ include_input: bool = False,
57
+ min_freq_log2: float = 0,
58
+ max_freq_log2: float = 12,
59
+ num_freqs: int = 32,
60
+ log_sampling: bool = True,
61
+ ):
62
+ super().__init__()
63
+ self.in_dim = in_dim
64
+ self.include_input = include_input
65
+ self.min_freq_log2 = min_freq_log2
66
+ self.max_freq_log2 = max_freq_log2
67
+ self.num_freqs = num_freqs
68
+ self.log_sampling = log_sampling
69
+ self.create_embedding_fn()
70
+
71
+ def create_embedding_fn(self):
72
+ d = self.in_dim
73
+ dim_out = 0
74
+ if self.include_input:
75
+ dim_out += d
76
+
77
+ min_freq = self.min_freq_log2
78
+ max_freq = self.max_freq_log2
79
+ N_freqs = self.num_freqs
80
+
81
+ if self.log_sampling:
82
+ freq_bands = 2.0 ** mx.linspace(min_freq, max_freq, num=N_freqs) # (nf,)
83
+ else:
84
+ freq_bands = mx.linspace(2.0**min_freq, 2.0**max_freq, num=N_freqs) # (nf,)
85
+
86
+ assert mx.isfinite(
87
+ freq_bands
88
+ ).all(), f"nan: {mx.isnan(freq_bands).any()} inf: {mx.isinf(freq_bands).any()}"
89
+
90
+ self.freq_bands = freq_bands
91
+ self.embed_dim = dim_out + d * self.freq_bands.size * 2
92
+
93
+ def __call__(
94
+ self,
95
+ pos,
96
+ ):
97
+ """
98
+ Get the positional encoding for each coordinate.
99
+ Args:
100
+ pos:
101
+ (*, in_dim)
102
+ Returns:
103
+ out:
104
+ (*, in_dimitional_encoding)
105
+ """
106
+
107
+ out = []
108
+ if self.include_input:
109
+ out = [pos] # (*, in_dim)
110
+
111
+ pos = pos[..., None] * self.freq_bands # (*b, d, nf)
112
+
113
+ out += [
114
+ mx.sin(pos).flatten(start_axis=-2), # (*b, d*nf)
115
+ mx.cos(pos).flatten(start_axis=-2), # (*b, d*nf)
116
+ ]
117
+
118
+ out = mx.concatenate(out, axis=-1) # (*b, 2 * in_dim * nf (+ in_dim))
119
+ return out
120
+
121
+
122
+ def compute_axial_cis(
123
+ ts,
124
+ in_dim: int,
125
+ dim: int,
126
+ theta: float = 100.0,
127
+ ):
128
+ B, N, D = ts.shape
129
+ freqs_all = []
130
+ interval = 2 * in_dim
131
+ for i in range(in_dim):
132
+ freq = 1.0 / (
133
+ theta
134
+ ** (
135
+ mx.arange(0, dim, interval)[: (dim // interval)].astype(mx.float32)
136
+ / dim
137
+ )
138
+ )
139
+ t = ts[..., i].flatten()
140
+ freq_i = mx.outer(t, freq)
141
+ freq_cis_i = polar(mx.ones_like(freq_i), freq_i)
142
+ freq_cis_i = freq_cis_i.reshape((B, N, -1))
143
+ freqs_all.append(freq_cis_i)
144
+ freqs_cis = mx.concatenate(freqs_all, axis=-1)
145
+ return freqs_cis
146
+
147
+
148
+ def polar(a, b):
149
+ return a * mx.exp(1j * b)
150
+
151
+
152
+ def view_as_complex(x):
153
+ x = x.astype(mx.float32)
154
+ x = x.reshape(*x.shape[:-1], -1, 2) # (..., dim/2, 2)
155
+ return x[..., 0] + 1j * x[..., 1] # real, imag
156
+
157
+
158
+ def view_as_real(input):
159
+ return mx.stack([input.real, input.imag], axis=-1) # (..., dim)
160
+
161
+
162
+ def apply_rotary_emb(xq: mx.array, xk: mx.array, freqs_cis: mx.array):
163
+ # xq, xk: shape (..., dim)
164
+ # freqs_cis: shape (..., dim // 2, 2) where last dim = [cos, sin]
165
+ xq_ = view_as_complex(xq)
166
+ xk_ = view_as_complex(xk)
167
+
168
+ # Apply complex multiplication
169
+ xq_out = xq_ * freqs_cis
170
+ xk_out = xk_ * freqs_cis
171
+
172
+ # Reconstruct to original shape
173
+ xq_out = view_as_real(xq_out).flatten(start_axis=3)
174
+ xk_out = view_as_real(xk_out).flatten(start_axis=3)
175
+
176
+ return xq_out.astype(xq.dtype), xk_out.astype(xk.dtype)
177
+
178
+
179
+ class AxialRotaryPositionEncoding(nn.Module):
180
+ def __init__(
181
+ self,
182
+ in_dim,
183
+ embed_dim,
184
+ num_heads,
185
+ base=100.0,
186
+ ):
187
+ super().__init__()
188
+ self.in_dim = in_dim
189
+ self.num_heads = num_heads
190
+ self.embed_dim = embed_dim // num_heads
191
+ self.base = base
192
+
193
+ def __call__(self, xq, xk, pos):
194
+ """
195
+ xq: [B, H, N, D]
196
+ xk: [B, H, N, D]
197
+ pos: [B, N, in_dim]
198
+ """
199
+ if pos.ndim == 2:
200
+ pos = pos[..., None]
201
+ freqs_cis = compute_axial_cis(pos, self.in_dim, self.embed_dim, self.base)
202
+ freqs_cis = mx.expand_dims(freqs_cis, axis=1)
203
+
204
+ return apply_rotary_emb(xq, xk, freqs_cis)
models/simplefold/mlx/sampler.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import mlx.core as mx
7
+ from tqdm import tqdm
8
+ from einops.array_api import repeat
9
+ from onescience.utils.simplefold.mlx_utils import center_random_augmentation
10
+
11
+
12
+ def logspace(start, end, steps, base=10.0, dtype=mx.float32):
13
+ # create a linear space between start and end
14
+ lin = mx.linspace(start, end, steps, dtype=dtype)
15
+ # raise base to that power
16
+ return mx.power(mx.array(base, dtype=dtype), lin)
17
+
18
+
19
+ class EMSampler:
20
+ """
21
+ A Euler-Maruyama solver for SDEs.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ num_timesteps=500,
27
+ t_start=1e-4,
28
+ tau=0.3,
29
+ log_timesteps=False,
30
+ w_cutoff=0.99,
31
+ ):
32
+ self.num_timesteps = num_timesteps
33
+ self.log_timesteps = log_timesteps
34
+ self.t_start = t_start
35
+ self.tau = tau
36
+ self.w_cutoff = w_cutoff
37
+
38
+ if self.log_timesteps:
39
+ t = 1.0 - logspace(-2, 0, steps=self.num_timesteps + 1)[::-1, ...]
40
+ t = t - mx.min(t)
41
+ t = t / mx.max(t)
42
+ self.steps = mx.clip(t, a_min=self.t_start, a_max=1.0)
43
+ else:
44
+ self.steps = mx.linspace(self.t_start, 1.0, num=self.num_timesteps + 1)
45
+
46
+ def diffusion_coefficient(self, t, eps=0.01):
47
+ # determine diffusion coefficient
48
+ w = (1.0 - t) / (t + eps)
49
+ if t >= self.w_cutoff:
50
+ w = 0.0
51
+ return w
52
+
53
+ def euler_maruyama_step(
54
+ self,
55
+ model_fn,
56
+ flow,
57
+ y,
58
+ t,
59
+ t_next,
60
+ batch,
61
+ ):
62
+ dt = t_next - t
63
+ eps = mx.random.normal(y.shape)
64
+
65
+ y = center_random_augmentation(
66
+ y,
67
+ batch["atom_pad_mask"],
68
+ augmentation=False,
69
+ centering=True,
70
+ )
71
+
72
+ batched_t = repeat(t, " -> b", b=y.shape[0])
73
+ velocity = model_fn(
74
+ noised_pos=y,
75
+ t=batched_t,
76
+ feats=batch,
77
+ )["predict_velocity"]
78
+
79
+ score = flow.compute_score_from_velocity(velocity, y, t)
80
+
81
+ diff_coeff = self.diffusion_coefficient(t)
82
+ drift = velocity + diff_coeff * score
83
+ mean_y = y + drift * dt
84
+ y_sample = mean_y + mx.sqrt(2.0 * dt * diff_coeff * self.tau) * eps
85
+
86
+ return y_sample
87
+
88
+ def sample(self, model_fn, flow, noise, batch):
89
+ sampling_timesteps = self.num_timesteps
90
+ steps = self.steps
91
+ y_sampled = noise
92
+ feats = batch
93
+
94
+ for i in tqdm(
95
+ range(sampling_timesteps),
96
+ desc="Sampling",
97
+ total=sampling_timesteps,
98
+ ):
99
+ t = steps[i]
100
+ t_next = steps[i + 1]
101
+
102
+ y_sampled = self.euler_maruyama_step(
103
+ model_fn,
104
+ flow,
105
+ y_sampled,
106
+ t,
107
+ t_next,
108
+ feats,
109
+ )
110
+ mx.eval(y_sampled)
111
+
112
+ return {"denoised_coords": y_sampled}
models/simplefold/simplefold.py ADDED
@@ -0,0 +1,781 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import os
7
+ import shutil
8
+ import copy
9
+ import numpy as np
10
+ from pathlib import Path
11
+ from einops import repeat
12
+
13
+ import torch
14
+ import torch.nn.functional as F
15
+ from torch.optim.optimizer import Optimizer
16
+ import torch.distributed as dist
17
+ from torch.distributed.fsdp import FullyShardedDataParallel
18
+ from torch.optim.swa_utils import AveragedModel
19
+ from torch.nn.utils import clip_grad_norm_
20
+
21
+ import lightning
22
+ import lightning.pytorch as pl
23
+ from fairscale.nn.data_parallel import FullyShardedDataParallel as FSDP
24
+ from fairscale.nn.wrap import enable_wrap, wrap
25
+
26
+ from onescience.utils.simplefold.esm_utils import _af2_to_esm, esm_registry
27
+ from onescience.datapipes.boltz_data_pipeline.types import Record, Structure
28
+ from onescience.utils.simplefold.boltz_utils import (
29
+ weighted_rigid_align,
30
+ center_random_augmentation,
31
+ process_structure,
32
+ save_structure
33
+ )
34
+
35
+
36
+ def logit_normal_sample(n=1, m=0.0, s=1.0):
37
+ # Logit-Normal Sampling from https://arxiv.org/pdf/2403.03206.pdf
38
+ u = torch.randn(n) * s + m
39
+ t = 1 / (1 + torch.exp(-u))
40
+ return t
41
+
42
+
43
+ def lddt_dist(dmat_predicted, dmat_true, mask, cutoff=15.0, per_atom=False):
44
+ # NOTE: the mask is a pairwise mask which should have the identity elements already masked out
45
+ # Compute mask over distances
46
+ dists_to_score = (dmat_true < cutoff).float() * mask
47
+ dist_l1 = torch.abs(dmat_true - dmat_predicted)
48
+
49
+ score = 0.25 * (
50
+ (dist_l1 < 0.5).float()
51
+ + (dist_l1 < 1.0).float()
52
+ + (dist_l1 < 2.0).float()
53
+ + (dist_l1 < 4.0).float()
54
+ )
55
+
56
+ # Normalize over the appropriate axes.
57
+ if per_atom:
58
+ mask_no_match = torch.sum(dists_to_score, dim=-1) != 0
59
+ norm = 1.0 / (1e-10 + torch.sum(dists_to_score, dim=-1))
60
+ score = norm * (1e-10 + torch.sum(dists_to_score * score, dim=-1))
61
+ return score, mask_no_match.float()
62
+ else:
63
+ norm = 1.0 / (1e-10 + torch.sum(dists_to_score, dim=(-2, -1)))
64
+ score = norm * (1e-10 + torch.sum(dists_to_score * score, dim=(-2, -1)))
65
+ total = torch.sum(dists_to_score, dim=(-1, -2))
66
+ return score, total
67
+
68
+
69
+ class SimpleFold(pl.LightningModule):
70
+ def __init__(
71
+ self,
72
+ architecture,
73
+ processor,
74
+ loss,
75
+ path,
76
+ sampler,
77
+ optimizer=None,
78
+ scheduler=None,
79
+ plddt_module=None,
80
+ ema_decay=0.999,
81
+ esm_model="esm2_3B",
82
+ aa_bolt_link=None,
83
+ use_rigid_align=True,
84
+ smooth_lddt_loss_weight=1.0,
85
+ lddt_cutoff=15.0,
86
+ clip_grad_norm_val=None,
87
+ lddt_weight_schedule=False,
88
+ plddt_training=False,
89
+ sample_dir='artifacts/',
90
+ ):
91
+ super().__init__()
92
+ self.save_hyperparameters(logger=False)
93
+
94
+ self.model = architecture
95
+ self.model_ema = AveragedModel(
96
+ self.model,
97
+ multi_avg_fn=torch.optim.swa_utils.get_ema_multi_avg_fn(
98
+ self.hparams.ema_decay
99
+ ),
100
+ use_buffers=True,
101
+ )
102
+ self.model_ema.eval()
103
+
104
+ self.loss = loss
105
+ self.path = path
106
+ self.sampler = sampler
107
+
108
+ self.use_rigid_align = use_rigid_align
109
+ self.lddt_cutoff = lddt_cutoff
110
+ self.smooth_lddt_loss_weight = smooth_lddt_loss_weight
111
+ self.use_smooth_lddt_loss = smooth_lddt_loss_weight > 0.0
112
+ self.lddt_weight_schedule = lddt_weight_schedule
113
+ self.plddt_training = plddt_training
114
+ self.sample_dir = sample_dir
115
+
116
+ self.aa_bolt_link = aa_bolt_link
117
+ self.nval_steps = 0
118
+
119
+ try:
120
+ self.t_eps = self.sampler.t_eps
121
+ except AttributeError:
122
+ self.t_eps = 0.0
123
+
124
+ self.use_esm = esm_model is not None
125
+ if self.use_esm:
126
+ self.esm_model, self.esm_dict = esm_registry[esm_model]()
127
+ self.esm_model.eval()
128
+ self.af2_to_esm = _af2_to_esm(self.esm_dict)
129
+ print(f"Using ESM model: {esm_model}")
130
+ else:
131
+ self.esm_model = None
132
+ self.esm_dict = None
133
+ self.af2_to_esm = None
134
+
135
+ self.plddt_module = plddt_module
136
+ if self.plddt_training:
137
+ assert self.plddt_module is not None, "PLDDT module must be provided for PLDDT training"
138
+ self.model.eval()
139
+
140
+ def register(self, name, tensor):
141
+ self.register_buffer(name, tensor.type(torch.float32))
142
+
143
+ def loss_masking(self, loss, atom_mask):
144
+ loss_mask = repeat(atom_mask, "b s -> b s d", d=loss.shape[-1])
145
+ loss *= loss_mask
146
+
147
+ denom = torch.sum(atom_mask, -1, keepdim=True)
148
+ denom = denom.unsqueeze(-1)
149
+ loss = torch.sum(loss, dim=1, keepdim=True) / denom
150
+ return loss
151
+
152
+ def smooth_lddt_loss(
153
+ self,
154
+ pred_coords,
155
+ true_coords,
156
+ # is_nucleotide,
157
+ coords_mask,
158
+ t,
159
+ ):
160
+ """Compute weighted alignment.
161
+
162
+ Parameters
163
+ ----------
164
+ pred_coords: torch.Tensor
165
+ The predicted atom coordinates
166
+ true_coords: torch.Tensor
167
+ The ground truth atom coordinates
168
+ coords_mask: torch.Tensor
169
+ The atoms mask
170
+
171
+ """
172
+ B, N, _ = true_coords.shape
173
+ true_dists = torch.cdist(true_coords, true_coords)
174
+
175
+ mask = (true_dists < self.lddt_cutoff).float()
176
+ mask = mask * (1 - torch.eye(pred_coords.shape[1], device=pred_coords.device))
177
+ mask = mask * (coords_mask.unsqueeze(-1) * coords_mask.unsqueeze(-2))
178
+
179
+ # Compute distances between all pairs of atoms
180
+ pred_dists = torch.cdist(pred_coords, pred_coords)
181
+ dist_diff = torch.abs(true_dists - pred_dists)
182
+
183
+ # Compute epsilon values
184
+ eps = (
185
+ (
186
+ (
187
+ F.sigmoid(0.5 - dist_diff)
188
+ + F.sigmoid(1.0 - dist_diff)
189
+ + F.sigmoid(2.0 - dist_diff)
190
+ + F.sigmoid(4.0 - dist_diff)
191
+ )
192
+ / 4.0
193
+ )
194
+ .view(B, N, N)
195
+ .mean(dim=0)
196
+ )
197
+
198
+ # Calculate masked averaging
199
+ num = (eps * mask).sum(dim=(-1, -2))
200
+ den = mask.sum(dim=(-1, -2)).clamp(min=1)
201
+ lddt = num / den
202
+ if self.lddt_weight_schedule:
203
+ t_weight = 1 + 8 * torch.relu(t - 0.5)
204
+ lddt = (1.0 - lddt) * t_weight
205
+ return lddt.mean()
206
+ else:
207
+ return (1.0 - lddt.mean()) * self.smooth_lddt_loss_weight
208
+
209
+ def plddt_loss(
210
+ self,
211
+ pred_lddt,
212
+ pred_atom_coords,
213
+ true_atom_coords,
214
+ true_coords_resolved_mask,
215
+ feats,
216
+ # multiplicity=1,
217
+ ):
218
+ """Compute plddt loss.
219
+
220
+ Parameters
221
+ ----------
222
+ pred_lddt: torch.Tensor
223
+ The plddt logits
224
+ pred_atom_coords: torch.Tensor
225
+ The predicted atom coordinates
226
+ true_atom_coords: torch.Tensor
227
+ The atom coordinates after symmetry correction
228
+ true_coords_resolved_mask: torch.Tensor
229
+ The resolved mask after symmetry correction
230
+ feats: Dict[str, torch.Tensor]
231
+ Dictionary containing the model input
232
+
233
+ Returns
234
+ -------
235
+ torch.Tensor
236
+ Plddt loss
237
+
238
+ """
239
+
240
+ # extract necessary features
241
+ atom_mask = true_coords_resolved_mask
242
+
243
+ R_set_to_rep_atom = feats["r_set_to_rep_atom"].float()
244
+ # R_set_to_rep_atom = R_set_to_rep_atom.repeat_interleave(multiplicity, 0).float()
245
+
246
+ token_type = feats["mol_type"]
247
+ # token_type = token_type.repeat_interleave(multiplicity, 0)
248
+ # is_nucleotide_token = (token_type == const.chain_type_ids["DNA"]).float() + (
249
+ # token_type == const.chain_type_ids["RNA"]
250
+ # ).float()
251
+
252
+ B = true_atom_coords.shape[0]
253
+
254
+ # atom_to_token = feats["atom_to_token"].float()
255
+ # atom_to_token = atom_to_token.repeat_interleave(multiplicity, 0)
256
+
257
+ token_to_rep_atom = feats["token_to_rep_atom"].float()
258
+ # token_to_rep_atom = token_to_rep_atom.repeat_interleave(multiplicity, 0)
259
+
260
+ true_token_coords = torch.bmm(token_to_rep_atom, true_atom_coords)
261
+ pred_token_coords = torch.bmm(token_to_rep_atom, pred_atom_coords)
262
+
263
+ # compute true lddt
264
+ true_d = torch.cdist(
265
+ true_token_coords,
266
+ torch.bmm(R_set_to_rep_atom, true_atom_coords),
267
+ )
268
+ pred_d = torch.cdist(
269
+ pred_token_coords,
270
+ torch.bmm(R_set_to_rep_atom, pred_atom_coords),
271
+ )
272
+
273
+ # compute mask
274
+ pair_mask = atom_mask.unsqueeze(-1) * atom_mask.unsqueeze(-2)
275
+ pair_mask = (
276
+ pair_mask
277
+ * (1 - torch.eye(pair_mask.shape[1], device=pair_mask.device))[None, :, :]
278
+ )
279
+ pair_mask = torch.einsum("bnm,bkm->bnk", pair_mask, R_set_to_rep_atom)
280
+ pair_mask = torch.bmm(token_to_rep_atom, pair_mask)
281
+ atom_mask = torch.bmm(token_to_rep_atom, atom_mask.unsqueeze(-1).float())
282
+ # is_nucleotide_R_element = torch.bmm(
283
+ # R_set_to_rep_atom, torch.bmm(atom_to_token, is_nucleotide_token.unsqueeze(-1))
284
+ # ).squeeze(-1)
285
+ # cutoff = 15 + 15 * is_nucleotide_R_element.reshape(B, 1, -1).repeat(
286
+ # 1, true_d.shape[1], 1
287
+ # )
288
+
289
+ # compute lddt
290
+ target_lddt, mask_no_match = lddt_dist(
291
+ pred_d, true_d, pair_mask, cutoff=15.0, per_atom=True
292
+ )
293
+
294
+ # compute loss
295
+ num_bins = pred_lddt.shape[-1]
296
+ bin_index = torch.floor(target_lddt * num_bins).long()
297
+ bin_index = torch.clamp(bin_index, max=(num_bins - 1))
298
+ lddt_one_hot = F.one_hot(bin_index, num_classes=num_bins)
299
+ errors = -1 * torch.sum(
300
+ lddt_one_hot * F.log_softmax(pred_lddt, dim=-1),
301
+ dim=-1,
302
+ )
303
+ atom_mask = atom_mask.squeeze(-1)
304
+ loss = torch.sum(errors * atom_mask * mask_no_match, dim=-1) / (
305
+ 1e-7 + torch.sum(atom_mask * mask_no_match, dim=-1)
306
+ )
307
+
308
+ # Average over the batch dimension
309
+ loss = torch.mean(loss)
310
+
311
+ self.log(
312
+ "loss/plddt",
313
+ loss.item(),
314
+ on_epoch=True,
315
+ logger=True,
316
+ prog_bar=True,
317
+ rank_zero_only=True,
318
+ )
319
+
320
+ return loss
321
+
322
+ def plddt_train_step(self, batch, batch_idx):
323
+ with torch.no_grad():
324
+ batch = self.processor.preprocess_training(
325
+ batch,
326
+ esm_model=self.esm_model,
327
+ esm_dict=self.esm_dict,
328
+ af2_to_esm=self.af2_to_esm,
329
+ )
330
+
331
+ noise = torch.randn_like(batch['coords']).to(self.device)
332
+
333
+ out_dict = self.sampler.sample(
334
+ self.model_ema.module.forward, self.path,
335
+ noise, batch
336
+ )
337
+ # out_dict = self.processor.postprocess(out_dict, batch)
338
+
339
+ # denoised_coords = center_of_mass_norm(
340
+ # out_dict["denoised_coords"], batch['atom_pad_mask']
341
+ # )
342
+ # true_coords = center_of_mass_norm(
343
+ # batch['coords'], batch['atom_pad_mask']
344
+ # )
345
+ denoised_coords = center_random_augmentation(
346
+ out_dict["denoised_coords"],
347
+ batch['atom_pad_mask'],
348
+ augmentation=False,
349
+ centering=True,
350
+ )
351
+ true_coords = center_random_augmentation(
352
+ batch['coords'],
353
+ batch['atom_pad_mask'],
354
+ augmentation=False,
355
+ centering=True,
356
+ )
357
+ out_dict["denoised_coords"] = denoised_coords * self.processor.scale
358
+ out_dict["coords"] = true_coords * self.processor.scale
359
+
360
+ out_dict["true_coords_resolved_mask"] = batch["atom_resolved_mask"]
361
+
362
+ t = torch.ones(batch['coords'].shape[0], device=self.device)
363
+ out_feat = self.model(denoised_coords, t, batch) # use unscaled coords
364
+
365
+ # Compute plddt loss
366
+ plddt_out_dict = self.plddt_module(
367
+ out_feat["latent"].detach(),
368
+ batch,
369
+ )
370
+
371
+ plddt_loss = self.plddt_loss(
372
+ plddt_out_dict["plddt_logits"],
373
+ out_dict["denoised_coords"],
374
+ out_dict["coords"],
375
+ out_dict["true_coords_resolved_mask"],
376
+ batch,
377
+ )
378
+
379
+ return plddt_loss
380
+
381
+ def flow_matching_train_step(self, batch, batch_idx):
382
+ batch = self.processor.preprocess_training(
383
+ batch,
384
+ esm_model=self.esm_model,
385
+ esm_dict=self.esm_dict,
386
+ af2_to_esm=self.af2_to_esm,
387
+ )
388
+
389
+ # timestep resampling
390
+ t_size = batch['coords'].shape[0]
391
+ t = 0.98 * logit_normal_sample(n=t_size, m=0.8, s=1.7) + 0.02 * torch.rand(t_size)
392
+ t = t.to(self.device)
393
+ t = t * (1 - 2 * self.t_eps) + self.t_eps
394
+
395
+ noise = torch.randn_like(batch['coords']).to(self.device)
396
+
397
+ _, y_t, v_t = self.path.interpolant(t, noise, batch["coords"])
398
+
399
+ out_dict = self.model(y_t, t, batch)
400
+
401
+ resolved_atom_mask = batch["atom_resolved_mask"].float()
402
+ align_weights = y_t.new_ones(y_t.shape[:2])
403
+
404
+ if self.use_rigid_align:
405
+ with torch.no_grad(), torch.autocast("cuda", enabled=False):
406
+ v_t = out_dict['predict_velocity'].detach().float()
407
+ denoised_coords = y_t + v_t * (1.0 - t[:, None, None])
408
+ coords = batch["coords"].detach().float()
409
+ coords_aligned = weighted_rigid_align(
410
+ coords,
411
+ denoised_coords.detach().float(),
412
+ align_weights.detach().float(),
413
+ mask=resolved_atom_mask.detach().float(),
414
+ )
415
+ _, _, v_t_aligned = self.path.interpolant(t, noise, coords_aligned)
416
+ target = v_t_aligned
417
+ else:
418
+ target = v_t
419
+
420
+ loss = F.mse_loss(out_dict['predict_velocity'], target, reduction='none')
421
+
422
+ loss_mask = resolved_atom_mask * align_weights
423
+ loss = self.loss_masking(loss, loss_mask)
424
+ loss = loss.mean()
425
+
426
+ self.log(
427
+ "loss/mse",
428
+ loss.item(),
429
+ on_epoch=True,
430
+ logger=True,
431
+ prog_bar=True,
432
+ rank_zero_only=True,
433
+ )
434
+
435
+ if self.use_smooth_lddt_loss:
436
+ # one-step Euler to get denoised coordinates
437
+ denoised_coords = y_t + \
438
+ out_dict['predict_velocity'] * (1.0 - t[:, None, None])
439
+
440
+ # rescale coordinates to angstroms
441
+ # denoised_coords = center_of_mass_norm(denoised_coords, batch['atom_pad_mask'])
442
+ # true_coords = center_of_mass_norm(batch['coords'], batch['atom_pad_mask'])
443
+ denoised_coords = center_random_augmentation(
444
+ denoised_coords,
445
+ batch['atom_pad_mask'],
446
+ augmentation=False,
447
+ centering=True,
448
+ )
449
+ true_coords = center_random_augmentation(
450
+ batch['coords'],
451
+ batch['atom_pad_mask'],
452
+ augmentation=False,
453
+ centering=True,
454
+ )
455
+ denoised_coords = denoised_coords * self.processor.scale
456
+ true_coords = true_coords * self.processor.scale
457
+
458
+ smooth_lddt_loss = self.smooth_lddt_loss(
459
+ denoised_coords,
460
+ true_coords,
461
+ resolved_atom_mask,
462
+ t,
463
+ )
464
+ loss += smooth_lddt_loss
465
+
466
+ self.log(
467
+ "loss/smooth_lddt",
468
+ smooth_lddt_loss.item(),
469
+ on_epoch=True,
470
+ logger=True,
471
+ prog_bar=True,
472
+ rank_zero_only=True,
473
+ )
474
+
475
+ self.log(
476
+ "loss/loss",
477
+ loss.item(),
478
+ on_epoch=True,
479
+ logger=True,
480
+ prog_bar=True,
481
+ rank_zero_only=True,
482
+ )
483
+
484
+ self.log(
485
+ "trainer/global_step",
486
+ self.global_step,
487
+ on_epoch=False,
488
+ logger=True,
489
+ prog_bar=False,
490
+ rank_zero_only=True,
491
+ )
492
+
493
+ self.global_training_step = self.trainer.global_step
494
+ self.epoch = self.trainer.current_epoch
495
+ self.world_size = self.trainer.world_size
496
+ return loss
497
+
498
+ def training_step(self, batch, batch_idx):
499
+ if self.plddt_training:
500
+ return self.plddt_train_step(batch, batch_idx)
501
+ else:
502
+ return self.flow_matching_train_step(batch, batch_idx)
503
+
504
+ @torch.no_grad()
505
+ def validation_step(self, batch, batch_idx):
506
+ # we skip validation step in training mode
507
+ return
508
+
509
+ def on_train_start(self):
510
+ global_seed = os.environ.get("PL_GLOBAL_SEED", 42)
511
+ pl.seed_everything(int(global_seed) + self.trainer.global_rank, True)
512
+ return
513
+
514
+ @torch.no_grad()
515
+ def predict_step(self, batch, batch_idx):
516
+ with torch.autocast(device_type='cuda', dtype=torch.float32):
517
+ batch = self.processor.preprocess_inference(
518
+ batch,
519
+ esm_model=self.esm_model,
520
+ esm_dict=self.esm_dict,
521
+ af2_to_esm=self.af2_to_esm,
522
+ )
523
+ num_repeats = batch.get("num_repeats", torch.tensor(1, device=self.device)).item()
524
+ multiplicity = batch["mol_type"].shape[0]
525
+ num_iter = np.ceil(num_repeats / multiplicity).astype(int)
526
+ print(f"Generating {num_repeats} samples with num_iter: {num_iter}, multiplicity: {multiplicity}")
527
+
528
+ # num_repeats is the total number of samples to generate for one protein
529
+ # multiplicity is the number of samples to generate at once
530
+
531
+ curr_idx = 0
532
+ for i in range(num_iter):
533
+ batch_in = copy.deepcopy(batch)
534
+ noise = torch.randn_like(batch_in['coords']).to(self.device)
535
+
536
+ out_dict = self.sampler.sample(
537
+ self.model_ema.module.forward, self.path,
538
+ noise, batch_in
539
+ )
540
+
541
+ if self.plddt_module is not None:
542
+ denoised_coords = center_random_augmentation(
543
+ out_dict["denoised_coords"],
544
+ batch['atom_pad_mask'],
545
+ augmentation=False,
546
+ centering=True,
547
+ )
548
+
549
+ t = torch.ones(batch['coords'].shape[0], device=self.device)
550
+ out_feat = self.model(denoised_coords, t, batch) # use unscaled coords
551
+
552
+ plddt_out_dict = self.plddt_module(
553
+ out_feat["latent"],
554
+ batch_in,
555
+ )
556
+ plddts = plddt_out_dict["plddt"] * 100.0
557
+ else:
558
+ plddts = None
559
+
560
+ out_dict = self.processor.postprocess(out_dict, batch_in)
561
+
562
+ record = Record(**batch_in['record'][0])
563
+ gt_coord = out_dict['coords']
564
+ sampled_coord = out_dict['denoised_coords']
565
+ pad_mask = batch_in['atom_pad_mask']
566
+
567
+ if num_repeats - curr_idx < multiplicity:
568
+ curr_num_copies = num_repeats - curr_idx
569
+ gt_coord = gt_coord[:curr_num_copies]
570
+ sampled_coord = sampled_coord[:curr_num_copies]
571
+ pad_mask = pad_mask[:curr_num_copies]
572
+ plddts = plddts[:curr_num_copies] if plddts is not None else None
573
+ else:
574
+ curr_num_copies = multiplicity
575
+
576
+ data_dir = self.trainer.datamodule.predict_dataloader().dataset.target_dir
577
+ sample_dir = self.sample_dir
578
+ path = Path(data_dir) / "structures" / f"{record.id}.npz"
579
+ structure: Structure = Structure.load(path)
580
+
581
+ for j in range(curr_num_copies):
582
+ file_id = j + curr_idx
583
+ try:
584
+ sampled_structure = copy.deepcopy(structure)
585
+ sampled_structure = process_structure(
586
+ sampled_structure, sampled_coord[j], pad_mask[j], record
587
+ )
588
+ # save mmcif structure
589
+ sampled_struct_dir = Path(sample_dir)
590
+ outname = f"{record.id}_sampled_{str(file_id)}"
591
+ save_structure(
592
+ sampled_structure, sampled_struct_dir, outname,
593
+ plddts=plddts[j] if plddts is not None else None,
594
+ output_format="mmcif",
595
+ )
596
+ # save pdb structure
597
+ save_structure(
598
+ sampled_structure, sampled_struct_dir, outname,
599
+ plddts=plddts[j] if plddts is not None else None,
600
+ output_format="pdb",
601
+ )
602
+ except:
603
+ print(f"Error processing {record.id}")
604
+ continue
605
+
606
+ curr_idx += curr_num_copies
607
+
608
+ self.world_size = self.trainer.world_size
609
+
610
+ return
611
+
612
+ def on_predict_epoch_end(self):
613
+ dist.barrier() # wait for all processes to finish
614
+ return
615
+
616
+ def reset_esm(self, esm_model: str):
617
+ self.esm_model, self.esm_dict = esm_registry[esm_model]()
618
+ self.esm_model.eval()
619
+ self.af2_to_esm = _af2_to_esm(self.esm_dict)
620
+ self.esm_model = self.esm_model.to(self.device)
621
+ self.af2_to_esm = self.af2_to_esm.to(self.device)
622
+ print(f"Successfully reset ESM model {esm_model}")
623
+
624
+ def setup(self, stage: str) -> None:
625
+ """Lightning hook that is called at the beginning of fit (train + validate), validate,
626
+ test, or predict.
627
+
628
+ This is a good hook when you need to build models dynamically or adjust something about
629
+ them. This hook is called on every process when using DDP.
630
+
631
+ :param stage: Either `"fit"`, `"validate"`, `"test"`, or `"predict"`.
632
+ """
633
+ self.processor = self.hparams.processor(device=self.device)
634
+
635
+ if self.use_esm:
636
+ if stage == "fit" and not isinstance(
637
+ self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
638
+ ):
639
+ # initialize the model with FSDP wrapper
640
+ fsdp_params = dict(
641
+ mixed_precision=True,
642
+ flatten_parameters=True,
643
+ state_dict_device=torch.device("cpu"), # reduce GPU mem usage
644
+ cpu_offload=True, # enable cpu offloading
645
+ fp32_reduce_scatter=True, # use fp32 reduce scatter
646
+ )
647
+ with enable_wrap(wrapper_cls=FSDP, **fsdp_params):
648
+ self.esm_model.eval()
649
+ # Wrap each layer in FSDP separately
650
+ for name, child in self.esm_model.named_children():
651
+ if name == "layers":
652
+ for layer_name, layer in child.named_children():
653
+ wrapped_layer = wrap(layer)
654
+ setattr(child, layer_name, wrapped_layer)
655
+ self.esm_model = wrap(self.esm_model)
656
+ self.af2_to_esm = self.af2_to_esm.to(self.device)
657
+ else:
658
+ self.esm_model = self.esm_model.to(self.device)
659
+ self.af2_to_esm = self.af2_to_esm.to(self.device)
660
+
661
+ if stage == "fit":
662
+ self.training_gpus = self.trainer.world_size
663
+ self.hparams["training_gpus"] = self.training_gpus
664
+
665
+ batch = next(iter(self.trainer.datamodule.train_dataloader()))
666
+
667
+ batch = self.processor.preprocess_training(
668
+ batch,
669
+ self.esm_model,
670
+ self.esm_dict,
671
+ self.af2_to_esm,
672
+ )
673
+ y = batch["coords"]
674
+ t = torch.zeros((y.shape[0])).cuda()
675
+
676
+ def on_train_batch_end(self, outputs, batch, batch_idx):
677
+ optimizer = self.optimizers()
678
+ self.log(
679
+ "trainer/lr",
680
+ optimizer.param_groups[0]["lr"],
681
+ on_epoch=True,
682
+ logger=True,
683
+ prog_bar=True,
684
+ rank_zero_only=True,
685
+ )
686
+
687
+ def on_before_optimizer_step(self, optimizer: Optimizer) -> None:
688
+
689
+ if isinstance(
690
+ self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
691
+ ):
692
+
693
+ with FullyShardedDataParallel.summon_full_params(
694
+ self.trainer.strategy.model, with_grads=True
695
+ ):
696
+ clip_grad_norm_(
697
+ self.trainer.strategy.model.model.parameters(),
698
+ self.hparams.clip_grad_norm_val,
699
+ norm_type=2.0,
700
+ error_if_nonfinite=True,
701
+ )
702
+
703
+ else:
704
+ clip_grad_norm_(
705
+ self.model.parameters(),
706
+ self.hparams.clip_grad_norm_val,
707
+ norm_type=2.0,
708
+ error_if_nonfinite=True,
709
+ )
710
+ return
711
+
712
+ def on_before_zero_grad(self, optimizer: Optimizer) -> None:
713
+ # if self.eval_ema:
714
+ if isinstance(
715
+ self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
716
+ ):
717
+
718
+ with FullyShardedDataParallel.summon_full_params(
719
+ self.trainer.strategy.model
720
+ ):
721
+ self.trainer.strategy.model.model_ema.update_parameters(
722
+ self.trainer.strategy.model.model
723
+ )
724
+
725
+ else:
726
+ self.model_ema.update_parameters(self.model)
727
+ return
728
+
729
+ def on_save_checkpoint(self, checkpoint) -> None:
730
+
731
+ if not isinstance(
732
+ self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
733
+ ):
734
+ layers_to_delete = []
735
+ for k in checkpoint["state_dict"].keys():
736
+ if k.startswith("esm_model._fsdp_wrapped_module"):
737
+ layers_to_delete.append(k)
738
+
739
+ for k in layers_to_delete:
740
+ del checkpoint["state_dict"][k]
741
+
742
+ else:
743
+ try:
744
+ del checkpoint["hyper_parameters"]
745
+ except:
746
+ print("No hyper_parameters in checkpoint")
747
+
748
+ return super().on_save_checkpoint(checkpoint)
749
+
750
+ def on_load_checkpoint(self, checkpoint) -> None:
751
+
752
+ if not isinstance(
753
+ self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
754
+ ):
755
+ self.training_gpus = checkpoint["hyper_parameters"]["training_gpus"]
756
+ self.fwd_flops = checkpoint["hyper_parameters"]["fwd_flops"]
757
+
758
+ if checkpoint["loops"] is not None:
759
+
760
+ self.trainer.fit_loop.load_state_dict(checkpoint["loops"]["fit_loop"])
761
+ self.trainer.validate_loop.load_state_dict(
762
+ checkpoint["loops"]["validate_loop"]
763
+ )
764
+ return super().on_load_checkpoint(checkpoint)
765
+
766
+ def configure_optimizers(self):
767
+ optimizer = self.hparams.optimizer(
768
+ params=self.trainer.model.parameters()
769
+ )
770
+
771
+ if self.hparams.scheduler is not None:
772
+ scheduler = self.hparams.scheduler(optimizer=optimizer)
773
+ return {
774
+ "optimizer": optimizer,
775
+ "lr_scheduler": {
776
+ "scheduler": scheduler,
777
+ "interval": "step",
778
+ "frequency": 1,
779
+ },
780
+ }
781
+ return {"optimizer": optimizer}
models/simplefold/torch/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
models/simplefold/torch/architecture.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import math
7
+ import torch
8
+ from torch import nn
9
+ from torch.nn import functional as F
10
+
11
+ from .layers import FinalLayer, ConditionEmbedder
12
+ from onescience.utils.simplefold.esm_utils import esm_model_dict
13
+
14
+
15
+ class FoldingDiT(nn.Module):
16
+ def __init__(
17
+ self,
18
+ trunk,
19
+ time_embedder,
20
+ aminoacid_pos_embedder,
21
+ pos_embedder,
22
+ atom_encoder_transformer,
23
+ atom_decoder_transformer,
24
+ hidden_size=1152,
25
+ num_heads=16,
26
+ atom_num_heads=4,
27
+ output_channels=3,
28
+ atom_hidden_size_enc=256,
29
+ atom_hidden_size_dec=256,
30
+ atom_n_queries_enc=32,
31
+ atom_n_keys_enc=128,
32
+ atom_n_queries_dec=32,
33
+ atom_n_keys_dec=128,
34
+ esm_model="esm2_3B",
35
+ esm_dropout_prob=0.0,
36
+ use_atom_mask=False,
37
+ use_length_condition=True,
38
+ ):
39
+ super().__init__()
40
+ self.pos_embedder = pos_embedder
41
+ pos_embed_channels = pos_embedder.embed_dim
42
+ self.aminoacid_pos_embedder = aminoacid_pos_embedder
43
+ aminoacid_pos_embed_channels = aminoacid_pos_embedder.embed_dim
44
+
45
+ self.time_embedder = time_embedder
46
+
47
+ self.atom_encoder_transformer = atom_encoder_transformer
48
+ self.atom_decoder_transformer = atom_decoder_transformer
49
+
50
+ self.trunk = trunk
51
+
52
+ self.hidden_size = hidden_size
53
+ self.output_channels = output_channels
54
+ self.num_heads = num_heads
55
+ self.atom_num_heads = atom_num_heads
56
+ self.use_atom_mask = use_atom_mask
57
+ self.esm_dropout_prob = esm_dropout_prob
58
+ self.use_length_condition = use_length_condition
59
+
60
+ esm_s_dim = esm_model_dict[esm_model]["esm_s_dim"]
61
+ esm_num_layers = esm_model_dict[esm_model]["esm_num_layers"]
62
+
63
+ self.atom_hidden_size_enc = atom_hidden_size_enc
64
+ self.atom_hidden_size_dec = atom_hidden_size_dec
65
+ self.atom_n_queries_enc = atom_n_queries_enc
66
+ self.atom_n_keys_enc = atom_n_keys_enc
67
+ self.atom_n_queries_dec = atom_n_queries_dec
68
+ self.atom_n_keys_dec = atom_n_keys_dec
69
+
70
+ atom_feat_dim = pos_embed_channels + aminoacid_pos_embed_channels + 427
71
+ self.atom_feat_proj = nn.Sequential(
72
+ nn.Linear(atom_feat_dim, hidden_size),
73
+ nn.LayerNorm(hidden_size),
74
+ nn.SiLU(),
75
+ )
76
+ self.atom_pos_proj = nn.Linear(pos_embed_channels, hidden_size, bias=False)
77
+
78
+ if self.use_length_condition:
79
+ self.length_embedder = nn.Sequential(
80
+ nn.Linear(1, hidden_size, bias=False),
81
+ nn.LayerNorm(hidden_size),
82
+ )
83
+
84
+ self.atom_in_proj = nn.Linear(hidden_size * 2, hidden_size, bias=False)
85
+
86
+ self.esm_s_combine = nn.Parameter(torch.zeros(esm_num_layers))
87
+ self.esm_s_proj = ConditionEmbedder(
88
+ input_dim=esm_s_dim,
89
+ hidden_size=hidden_size,
90
+ dropout_prob=self.esm_dropout_prob,
91
+ )
92
+ latent_cat_dim = hidden_size * 2
93
+ self.esm_cat_proj = nn.Linear(latent_cat_dim, hidden_size)
94
+
95
+ self.context2atom_proj = nn.Sequential(
96
+ nn.Linear(hidden_size, self.atom_hidden_size_enc),
97
+ nn.LayerNorm(self.atom_hidden_size_enc),
98
+ )
99
+ self.atom2latent_proj = nn.Sequential(
100
+ nn.Linear(self.atom_hidden_size_enc, hidden_size),
101
+ nn.LayerNorm(hidden_size),
102
+ )
103
+ self.atom_enc_cond_proj = nn.Sequential(
104
+ nn.Linear(hidden_size, self.atom_hidden_size_enc),
105
+ nn.LayerNorm(self.atom_hidden_size_enc),
106
+ )
107
+ self.atom_dec_cond_proj = nn.Sequential(
108
+ nn.Linear(hidden_size, self.atom_hidden_size_dec),
109
+ nn.LayerNorm(self.atom_hidden_size_dec),
110
+ )
111
+
112
+ self.latent2atom_proj = nn.Sequential(
113
+ nn.Linear(hidden_size, hidden_size),
114
+ nn.SiLU(),
115
+ nn.LayerNorm(hidden_size),
116
+ nn.Linear(hidden_size, self.atom_hidden_size_dec),
117
+ )
118
+
119
+ self.final_layer = FinalLayer(
120
+ self.atom_hidden_size_dec,
121
+ output_channels,
122
+ c_dim=hidden_size
123
+ )
124
+
125
+ def create_local_attn_bias(
126
+ self, n: int, n_queries: int, n_keys: int, inf: float = 1e10, device: torch.device = None
127
+ ) -> torch.Tensor:
128
+ """Create local attention bias based on query window n_queries and kv window n_keys.
129
+
130
+ Args:
131
+ n (int): the length of quiries
132
+ n_queries (int): window size of quiries
133
+ n_keys (int): window size of keys/values
134
+ inf (float, optional): the inf to mask attention. Defaults to 1e10.
135
+ device (torch.device, optional): cuda|cpu|None. Defaults to None.
136
+
137
+ Returns:
138
+ torch.Tensor: the diagonal-like global attention bias
139
+ """
140
+ n_trunks = int(math.ceil(n / n_queries))
141
+ padded_n = n_trunks * n_queries
142
+ attn_mask = torch.zeros(padded_n, padded_n, device=device)
143
+ for block_index in range(0, n_trunks):
144
+ i = block_index * n_queries
145
+ j1 = max(0, n_queries * block_index - (n_keys - n_queries) // 2)
146
+ j2 = n_queries * block_index + (n_queries + n_keys) // 2
147
+ attn_mask[i : i + n_queries, j1:j2] = 1.0
148
+ attn_bias = (1 - attn_mask) * -inf
149
+ return attn_bias.to(device=device)[:n, :n]
150
+
151
+ def create_atom_attn_mask(
152
+ self,
153
+ feats,
154
+ natoms,
155
+ atom_n_queries=None,
156
+ atom_n_keys=None,
157
+ inf: float = 1e10
158
+ ) -> torch.Tensor:
159
+ if atom_n_queries is not None and atom_n_keys is not None:
160
+ atom_attn_mask = self.create_local_attn_bias(
161
+ n=natoms,
162
+ n_queries=atom_n_queries,
163
+ n_keys=atom_n_keys,
164
+ device=feats["ref_pos"].device,
165
+ inf=inf,
166
+ )
167
+ else:
168
+ atom_attn_mask = None
169
+
170
+ return atom_attn_mask
171
+
172
+ def forward(self, noised_pos, t, feats, self_cond=None):
173
+ B, N, _ = feats["ref_pos"].shape
174
+ M = feats["mol_type"].shape[1]
175
+ atom_to_token = feats["atom_to_token"].float() # [B, N, M]
176
+ atom_to_token_idx = feats["atom_to_token_idx"]
177
+ ref_space_uid = feats["ref_space_uid"]
178
+
179
+ # create atom attention masks
180
+ atom_attn_mask_enc = self.create_atom_attn_mask(
181
+ feats,
182
+ natoms=N,
183
+ atom_n_queries=self.atom_n_queries_enc,
184
+ atom_n_keys=self.atom_n_keys_enc,
185
+ )
186
+ atom_attn_mask_dec = self.create_atom_attn_mask(
187
+ feats,
188
+ natoms=N,
189
+ atom_n_queries=self.atom_n_queries_dec,
190
+ atom_n_keys=self.atom_n_keys_dec,
191
+ )
192
+
193
+ # create condition embeddings for AdaLN
194
+ c_emb = self.time_embedder(t) # (B, D)
195
+ if self.use_length_condition:
196
+ length = feats["max_num_tokens"].float().unsqueeze(-1)
197
+ c_emb = c_emb + self.length_embedder(torch.log(length))
198
+
199
+ # create atom features
200
+ mol_type = feats["mol_type"]
201
+ mol_type = F.one_hot(mol_type, num_classes=4).float() # [B, M, 4]
202
+ res_type = feats["res_type"].float() # [B, M, 33]
203
+ pocket_feature = feats["pocket_feature"].float() # [B, M, 4]
204
+ res_feat = torch.cat(
205
+ [mol_type, res_type, pocket_feature],
206
+ dim=-1) # [B, M, 41]
207
+ atom_feat_from_res = torch.bmm(atom_to_token, res_feat) # [B, N, 41]
208
+ atom_res_pos = self.aminoacid_pos_embedder(
209
+ pos=atom_to_token_idx.unsqueeze(-1).float()
210
+ )
211
+ ref_pos_emb = self.pos_embedder(pos=feats["ref_pos"])
212
+ atom_feat = torch.cat(
213
+ [
214
+ ref_pos_emb, # (B, N, PD1)
215
+ atom_feat_from_res, # (B, N, 41)
216
+ atom_res_pos, # (B, N, PD2)
217
+ feats["ref_charge"].unsqueeze(-1), # (B, N, 1)
218
+ feats["atom_pad_mask"].unsqueeze(-1), # (B, N, 1)
219
+ feats["ref_element"], # (B, N, 128)
220
+ feats["ref_atom_name_chars"].reshape(B, N, 4 * 64), # (B, N, 256)
221
+ ],
222
+ dim=-1,
223
+ ) # (B, N, PD1+PD2+427)
224
+ atom_feat = self.atom_feat_proj(atom_feat) # (B, N, D)
225
+
226
+ atom_coord = self.pos_embedder(pos=noised_pos) # (B, N, PD1)
227
+ atom_coord = self.atom_pos_proj(atom_coord) # (B, N, D)
228
+
229
+ atom_in = torch.cat([atom_feat, atom_coord], dim=-1)
230
+ atom_in = self.atom_in_proj(atom_in) # (B, N, D)
231
+
232
+ # position embeddings for Axial RoPE
233
+ atom_pe_pos = torch.cat(
234
+ [
235
+ ref_space_uid.unsqueeze(-1).float(), # (B, N, 1)
236
+ feats["ref_pos"], # (B, N, 3)
237
+ ],
238
+ dim=-1,
239
+ ) # (B, N, 4)
240
+ token_pe_pos = torch.cat(
241
+ [
242
+ feats["residue_index"].unsqueeze(-1).float(), # (B, M, 1)
243
+ feats["entity_id"].unsqueeze(-1).float(), # (B, M, 1)
244
+ feats["asym_id"].unsqueeze(-1).float(), # (B, M, 1)
245
+ feats["sym_id"].unsqueeze(-1).float(), # (B, M, 1)
246
+ ],
247
+ dim=-1,
248
+ ) # (B, M, 4)
249
+
250
+ # atom encoder
251
+ atom_c_emb_enc = self.atom_enc_cond_proj(c_emb)
252
+ atom_latent = self.context2atom_proj(atom_in)
253
+ atom_latent = self.atom_encoder_transformer(
254
+ latents=atom_latent,
255
+ c=atom_c_emb_enc,
256
+ attention_mask=atom_attn_mask_enc,
257
+ pos=atom_pe_pos,
258
+ )
259
+ atom_latent = self.atom2latent_proj(atom_latent)
260
+
261
+ # grouping: aggregate atom tokens to residue tokens
262
+ atom_to_token_mean = atom_to_token / (
263
+ atom_to_token.sum(dim=1, keepdim=True) + 1e-6
264
+ )
265
+ latent = torch.bmm(atom_to_token_mean.transpose(1, 2), atom_latent)
266
+ assert latent.shape[1] == M
267
+
268
+ esm_s = (self.esm_s_combine.softmax(0).unsqueeze(0) @ feats['esm_s']).squeeze(2)
269
+ force_drop_ids = feats.get("force_drop_ids", None)
270
+ esm_emb = self.esm_s_proj(esm_s, self.training, force_drop_ids)
271
+ assert esm_emb.shape[1] == latent.shape[1]
272
+
273
+ latent = self.esm_cat_proj(torch.cat([latent, esm_emb], dim=-1))
274
+
275
+ # residue trunk
276
+ latent = self.trunk(
277
+ latents=latent,
278
+ c=c_emb,
279
+ attention_mask=None,
280
+ pos=token_pe_pos,
281
+ )
282
+
283
+ # ungrouping: broadcast residue tokens to atom tokens
284
+ output = torch.bmm(atom_to_token, latent)
285
+ assert output.shape[1] == N
286
+
287
+ # add skip connection
288
+ output = output + atom_latent
289
+ output = self.latent2atom_proj(output)
290
+
291
+ # atom decoder
292
+ atom_c_emb_dec = self.atom_dec_cond_proj(c_emb)
293
+ output = self.atom_decoder_transformer(
294
+ latents=output,
295
+ c=atom_c_emb_dec,
296
+ attention_mask=atom_attn_mask_dec,
297
+ pos=atom_pe_pos,
298
+ )
299
+ output = self.final_layer(output, c=c_emb)
300
+
301
+ return {
302
+ "predict_velocity": output,
303
+ "latent": latent,
304
+ }
models/simplefold/torch/layers.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import math
7
+ from einops import rearrange
8
+ import torch
9
+ from torch import nn
10
+ import torch.nn.functional as F
11
+
12
+
13
+ def modulate(x, shift, scale):
14
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
15
+
16
+
17
+ #################################################################################
18
+ # Attention Layers #
19
+ #################################################################################
20
+
21
+
22
+ class SelfAttentionLayer(nn.Module):
23
+ def __init__(
24
+ self,
25
+ hidden_size,
26
+ num_heads=8,
27
+ qkv_bias=False,
28
+ qk_scale=None,
29
+ attn_drop=0.0,
30
+ proj_drop=0.0,
31
+ use_bias=True,
32
+ qk_norm=True,
33
+ pos_embedder=None,
34
+ linear_target: nn.Module = nn.Linear,
35
+ ):
36
+ super().__init__()
37
+ self.num_heads = num_heads
38
+ head_dim = hidden_size // num_heads
39
+ self.scale = qk_scale or head_dim**-0.5
40
+
41
+ self.qkv = linear_target(hidden_size, hidden_size * 3, bias=qkv_bias)
42
+ self.attn_drop = nn.Dropout(attn_drop)
43
+ self.proj = linear_target(hidden_size, hidden_size, bias=use_bias)
44
+ self.proj_drop = nn.Dropout(proj_drop)
45
+
46
+ self.q_norm = RMSNorm(head_dim) if qk_norm else nn.Identity()
47
+ self.k_norm = RMSNorm(head_dim) if qk_norm else nn.Identity()
48
+
49
+ self.pos_embedder = pos_embedder
50
+
51
+ def forward(self, x, **kwargs):
52
+ B, N, C = x.shape
53
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
54
+ pos = kwargs.get("pos")
55
+
56
+ qkv = rearrange(qkv, "b n t h c -> t b h n c")
57
+ q, k, v = (
58
+ qkv[0],
59
+ qkv[1],
60
+ qkv[2],
61
+ ) # make torchscript happy (cannot use tensor as tuple)
62
+
63
+ q, k = self.q_norm(q), self.k_norm(k)
64
+
65
+ if self.pos_embedder and pos is not None:
66
+ q, k = self.pos_embedder(q, k, pos)
67
+
68
+ attn = (q @ k.transpose(-2, -1)) * self.scale
69
+ attn = attn.softmax(dim=-1)
70
+ attn = self.attn_drop(attn)
71
+
72
+ x = (attn @ v).transpose(1, 2).reshape(B, N, C)
73
+ x = self.proj(x)
74
+ x = self.proj_drop(x)
75
+ return x
76
+
77
+
78
+ class EfficientSelfAttentionLayer(SelfAttentionLayer):
79
+ """Started from https://github.com/facebookresearch/dinov2/blob/main/dinov2/layers/attention.py"""
80
+
81
+ def __init__(
82
+ self,
83
+ *args,
84
+ **kwargs,
85
+ ):
86
+ super().__init__(*args, **kwargs)
87
+
88
+ def forward(self, x, **kwargs):
89
+ B, N, C = x.shape
90
+ attn_mask = kwargs.get("attention_mask")
91
+ pos = kwargs.get("pos")
92
+
93
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
94
+ qkv = rearrange(qkv, "b n t h c -> t b h n c")
95
+ q, k, v = qkv.unbind(0)
96
+
97
+ if attn_mask is not None:
98
+ attn_mask = attn_mask.to(dtype=q.dtype)
99
+
100
+ if self.pos_embedder and pos is not None:
101
+ q, k = self.pos_embedder(q, k, pos)
102
+
103
+ q, k = self.q_norm(q), self.k_norm(k)
104
+ v1 = v.to(dtype = q.dtype)
105
+ x = nn.functional.scaled_dot_product_attention(q, k, v1, attn_mask=attn_mask)
106
+
107
+ x = x.transpose(1, 2).reshape(B, N, C)
108
+ x = self.proj(x)
109
+ x = self.proj_drop(x)
110
+
111
+ return x
112
+
113
+
114
+ #################################################################################
115
+ # FeedForward Layer #
116
+ #################################################################################
117
+
118
+
119
+ class SwiGLUFeedForward(nn.Module):
120
+ def __init__(self, dim, hidden_dim, multiple_of=256):
121
+ super().__init__()
122
+ hidden_dim = int(2 * hidden_dim / 3)
123
+ hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
124
+
125
+ self.w1 = nn.Linear(dim, hidden_dim, bias=False)
126
+ self.w2 = nn.Linear(hidden_dim, dim, bias=True)
127
+ self.w3 = nn.Linear(dim, hidden_dim, bias=False)
128
+
129
+ self.reset_parameters()
130
+
131
+ def reset_parameters(self):
132
+ torch.nn.init.xavier_uniform_(self.w1.weight)
133
+ torch.nn.init.xavier_uniform_(self.w2.weight)
134
+ torch.nn.init.xavier_uniform_(self.w3.weight)
135
+ if self.w1.bias is not None:
136
+ torch.nn.init.constant_(self.w1.bias, 0)
137
+ if self.w2.bias is not None:
138
+ torch.nn.init.constant_(self.w2.bias, 0)
139
+ if self.w3.bias is not None:
140
+ torch.nn.init.constant_(self.w3.bias, 0)
141
+
142
+ def forward(self, x):
143
+ return self.w2(F.silu(self.w1(x)) * self.w3(x))
144
+
145
+
146
+ #################################################################################
147
+ # Utility Layers #
148
+ #################################################################################
149
+
150
+
151
+ class TimestepEmbedder(nn.Module):
152
+ """
153
+ Embeds scalar timesteps into vector representations.
154
+ """
155
+
156
+ def __init__(self, hidden_size, frequency_embedding_size=256):
157
+ super().__init__()
158
+ self.mlp = nn.Sequential(
159
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
160
+ nn.SiLU(),
161
+ nn.Linear(hidden_size, hidden_size, bias=True),
162
+ )
163
+ self.frequency_embedding_size = frequency_embedding_size
164
+ self.initialize_weights()
165
+
166
+ def initialize_weights(self):
167
+ nn.init.normal_(self.mlp[0].weight, std=0.02)
168
+ nn.init.normal_(self.mlp[2].weight, std=0.02)
169
+
170
+ @staticmethod
171
+ def timestep_embedding(t, dim, max_period=10000):
172
+ """
173
+ Create sinusoidal timestep embeddings.
174
+ :param t: a 1-D Tensor of N indices, one per batch element.
175
+ These may be fractional.
176
+ :param dim: the dimension of the output.
177
+ :param max_period: controls the minimum frequency of the embeddings.
178
+ :return: an (N, D) Tensor of positional embeddings.
179
+ """
180
+ # https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
181
+ half = dim // 2
182
+ freqs = torch.exp(
183
+ -math.log(max_period)
184
+ * torch.arange(start=0, end=half, dtype=torch.float32)
185
+ / half
186
+ ).to(device=t.device)
187
+ args = t[:, None].float() * freqs[None]
188
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
189
+ if dim % 2:
190
+ embedding = torch.cat(
191
+ [embedding, torch.zeros_like(embedding[:, :1])], dim=-1
192
+ )
193
+ return embedding
194
+
195
+ def forward(self, t):
196
+ t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
197
+ t_emb = self.mlp(t_freq)
198
+ return t_emb
199
+
200
+
201
+ class ConditionEmbedder(nn.Module):
202
+ """
203
+ Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance.
204
+ """
205
+ def __init__(self, input_dim, hidden_size, dropout_prob):
206
+ super().__init__()
207
+ self.proj = nn.Sequential(
208
+ nn.Linear(input_dim, hidden_size),
209
+ nn.LayerNorm(hidden_size),
210
+ nn.SiLU(),
211
+ )
212
+ self.dropout_prob = dropout_prob
213
+ self.null_token = nn.Parameter(torch.randn(input_dim), requires_grad=True)
214
+
215
+ def token_drop(self, cond, force_drop_ids=None):
216
+ """
217
+ cond: (B, N, D)
218
+ Drops conditions to enable classifier-free guidance.
219
+ """
220
+ if force_drop_ids is None:
221
+ drop_ids = torch.rand(cond.shape[0], device=cond.device) < self.dropout_prob
222
+ else:
223
+ drop_ids = force_drop_ids
224
+ cond[drop_ids] = self.null_token[None, None, :]
225
+ return cond
226
+
227
+ def forward(self, cond, train, force_drop_ids=None):
228
+ use_dropout = self.dropout_prob > 0
229
+ if (train and use_dropout) or (force_drop_ids is not None):
230
+ cond = self.token_drop(cond, force_drop_ids)
231
+ embeddings = self.proj(cond)
232
+ return embeddings
233
+
234
+
235
+ class FinalLayer(nn.Module):
236
+ """
237
+ The final layer of DiT.
238
+ """
239
+
240
+ def __init__(self, hidden_size, out_channels, c_dim=None):
241
+ super().__init__()
242
+ self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
243
+ self.linear = nn.Linear(hidden_size, out_channels, bias=True)
244
+ self.adaLN_modulation = nn.Sequential(
245
+ nn.SiLU(), nn.Linear(c_dim, 2 * hidden_size, bias=True)
246
+ )
247
+ self.initialize_weights()
248
+
249
+ def initialize_weights(self):
250
+ # Initialize transformer layers:
251
+ def _basic_init(module):
252
+ if isinstance(module, nn.Linear):
253
+ torch.nn.init.xavier_uniform_(module.weight)
254
+ if module.bias is not None:
255
+ nn.init.constant_(module.bias, 0)
256
+
257
+ self.apply(_basic_init)
258
+
259
+ # Zero-out output layers:
260
+ nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
261
+ nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
262
+ nn.init.constant_(self.linear.weight, 0)
263
+ nn.init.constant_(self.linear.bias, 0)
264
+
265
+ def forward(self, x, c):
266
+ shift, scale = self.adaLN_modulation(c).chunk(2, dim=1)
267
+ x = modulate(self.norm_final(x), shift, scale)
268
+ x = self.linear(x)
269
+ return x
270
+
271
+
272
+ class RMSNorm(nn.Module):
273
+ def __init__(self, d, p=-1.0, eps=1e-8, bias=False):
274
+ """
275
+ Root Mean Square Layer Normalization
276
+ :param d: model size
277
+ :param p: partial RMSNorm, valid value [0, 1], default -1.0 (disabled)
278
+ :param eps: epsilon value, default 1e-8
279
+ :param bias: whether use bias term for RMSNorm, disabled by
280
+ default because RMSNorm doesn't enforce re-centering invariance.
281
+ """
282
+ super(RMSNorm, self).__init__()
283
+
284
+ self.eps = eps
285
+ self.d = d
286
+ self.p = p
287
+ self.bias = bias
288
+
289
+ self.scale = nn.Parameter(torch.ones(d))
290
+ self.register_parameter("scale", self.scale)
291
+
292
+ if self.bias:
293
+ self.offset = nn.Parameter(torch.zeros(d))
294
+ self.register_parameter("offset", self.offset)
295
+
296
+ def forward(self, x):
297
+ if self.p < 0.0 or self.p > 1.0:
298
+ norm_x = x.norm(2, dim=-1, keepdim=True, dtype=x.dtype)
299
+ d_x = self.d
300
+ else:
301
+ partial_size = int(self.d * self.p)
302
+ partial_x, _ = torch.split(x, [partial_size, self.d - partial_size], dim=-1)
303
+
304
+ norm_x = partial_x.norm(2, dim=-1, keepdim=True, dtype=x.dtype)
305
+ d_x = partial_size
306
+
307
+ rms_x = norm_x * d_x ** (-1.0 / 2)
308
+ x_normed = x / (rms_x + self.eps)
309
+
310
+ if self.bias:
311
+ return self.scale * x_normed + self.offset
312
+
313
+ return self.scale * x_normed
models/simplefold/torch/pos_embed.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import math
7
+ import torch
8
+ from torch import nn
9
+
10
+
11
+ class AbsolutePositionEncoding(nn.Module):
12
+ def __init__(self, in_dim, embed_dim, include_input=False):
13
+ super().__init__()
14
+ self.in_dim = in_dim
15
+ self.hidden_dim = embed_dim
16
+ self.include_input = include_input
17
+ assert embed_dim % in_dim == 0, "embed_dim must be divisible by in_dim"
18
+ self.embed_dim = embed_dim + in_dim if include_input else embed_dim
19
+
20
+ def forward(self, pos):
21
+ pos_embs = []
22
+ for i in range(self.in_dim):
23
+ pe = self.get_1d_pos_embed(pos[..., i])
24
+ pos_embs.append(pe)
25
+ if self.include_input:
26
+ pos_embs.append(pos)
27
+ pos_embs = torch.cat(pos_embs, dim=-1)
28
+ return pos_embs
29
+
30
+ def get_1d_pos_embed(self, pos):
31
+ """
32
+ https://github.com/facebookresearch/DiT/blob/main/models.py#L303
33
+ """
34
+ embed_dim = self.hidden_dim // (self.in_dim * 2)
35
+ omega = 2 ** torch.linspace(0, math.log(224, 2) - 1, embed_dim).to(pos.device)
36
+ omega *= torch.pi
37
+
38
+ if len(pos.shape) == 1:
39
+ out = torch.einsum("m,d->md", pos, omega) # (M, D/2), outer product
40
+ elif len(pos.shape) == 2:
41
+ out = torch.einsum("nm,d->nmd", pos, omega)
42
+
43
+ emb_sin = torch.sin(out) # (*, M, D/2)
44
+ emb_cos = torch.cos(out) # (*, M, D/2)
45
+ emb = torch.cat([emb_sin, emb_cos], dim=-1) # (*, M, D)
46
+ return emb
47
+
48
+
49
+ class FourierPositionEncoding(torch.nn.Module):
50
+ def __init__(
51
+ self,
52
+ in_dim: int,
53
+ include_input: bool = False,
54
+ min_freq_log2: float = 0,
55
+ max_freq_log2: float = 12,
56
+ num_freqs: int = 32,
57
+ log_sampling: bool = True,
58
+ ):
59
+ super().__init__()
60
+ self.in_dim = in_dim
61
+ self.include_input = include_input
62
+ self.min_freq_log2 = min_freq_log2
63
+ self.max_freq_log2 = max_freq_log2
64
+ self.num_freqs = num_freqs
65
+ self.log_sampling = log_sampling
66
+ self.create_embedding_fn()
67
+
68
+ def create_embedding_fn(self):
69
+ d = self.in_dim
70
+ dim_out = 0
71
+ if self.include_input:
72
+ dim_out += d
73
+
74
+ min_freq = self.min_freq_log2
75
+ max_freq = self.max_freq_log2
76
+ N_freqs = self.num_freqs
77
+
78
+ if self.log_sampling:
79
+ freq_bands = 2.0 ** torch.linspace(
80
+ min_freq, max_freq, steps=N_freqs
81
+ ) # (nf,)
82
+ else:
83
+ freq_bands = torch.linspace(
84
+ 2.0**min_freq, 2.0**max_freq, steps=N_freqs
85
+ ) # (nf,)
86
+
87
+ assert (
88
+ freq_bands.isfinite().all()
89
+ ), f"nan: {freq_bands.isnan().any()} inf: {freq_bands.isinf().any()}"
90
+
91
+ self.register_buffer("freq_bands", freq_bands) # (nf,)
92
+ self.embed_dim = dim_out + d * self.freq_bands.numel() * 2
93
+
94
+ def forward(
95
+ self,
96
+ pos: torch.Tensor,
97
+ ):
98
+ """
99
+ Get the positional encoding for each coordinate.
100
+ Args:
101
+ pos:
102
+ (*, in_dim)
103
+ Returns:
104
+ out:
105
+ (*, in_dimitional_encoding)
106
+ """
107
+
108
+ out = []
109
+ if self.include_input:
110
+ out = [pos] # (*, in_dim)
111
+
112
+ pos = pos.unsqueeze(-1) * self.freq_bands # (*b, d, nf)
113
+
114
+ out += [
115
+ torch.sin(pos).flatten(start_dim=-2), # (*b, d*nf)
116
+ torch.cos(pos).flatten(start_dim=-2), # (*b, d*nf)
117
+ ]
118
+
119
+ out = torch.cat(out, dim=-1) # (*b, 2 * in_dim * nf (+ in_dim))
120
+ return out
121
+
122
+
123
+ def compute_axial_cis(
124
+ ts: torch.Tensor,
125
+ in_dim: int,
126
+ dim: int,
127
+ theta: float = 100.0,
128
+ ):
129
+ B, N, D = ts.shape
130
+ freqs_all = []
131
+ interval = 2 * in_dim
132
+ for i in range(in_dim):
133
+ freq = 1.0 / (
134
+ theta ** (torch.arange(0, dim, interval)[: (dim // interval)].float() / dim)
135
+ ).to(ts.device)
136
+ t = ts[..., i].flatten()
137
+ freq_i = torch.outer(t, freq)
138
+ freq_cis_i = torch.polar(torch.ones_like(freq_i), freq_i)
139
+ freq_cis_i = freq_cis_i.view(B, N, -1)
140
+ freqs_all.append(freq_cis_i)
141
+ freqs_cis = torch.cat(freqs_all, dim=-1)
142
+ return freqs_cis
143
+
144
+
145
+ def apply_rotary_emb(xq: torch.Tensor, xk: torch.Tensor, freqs_cis: torch.Tensor):
146
+ xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
147
+ xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
148
+ xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
149
+ xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
150
+ return xq_out.type_as(xq).to(xq.device), xk_out.type_as(xk).to(xk.device)
151
+
152
+
153
+ class AxialRotaryPositionEncoding(nn.Module):
154
+ def __init__(
155
+ self,
156
+ in_dim,
157
+ embed_dim,
158
+ num_heads,
159
+ base=100.0,
160
+ ):
161
+ super().__init__()
162
+ self.in_dim = in_dim
163
+ self.num_heads = num_heads
164
+ self.embed_dim = embed_dim // num_heads
165
+ self.base = base
166
+
167
+ def forward(self, xq, xk, pos):
168
+ """
169
+ xq: [B, H, N, D]
170
+ xk: [B, H, N, D]
171
+ pos: [B, N, in_dim]
172
+ """
173
+ if pos.ndim == 2:
174
+ pos = pos.unsqueeze(-1)
175
+ freqs_cis = compute_axial_cis(pos, self.in_dim, self.embed_dim, self.base)
176
+ freqs_cis = freqs_cis.unsqueeze(1)
177
+ return apply_rotary_emb(xq, xk, freqs_cis.to(xq.device))
scripts/__pycache__/inference.cpython-311.pyc ADDED
Binary file (22.4 kB). View file
 
scripts/__pycache__/train.cpython-310.pyc ADDED
Binary file (3.82 kB). View file
 
scripts/__pycache__/train_fsdp.cpython-310.pyc ADDED
Binary file (4.29 kB). View file
 
scripts/evaluate.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import hydra
7
+ import torch
8
+ from omegaconf import OmegaConf
9
+ import lightning.pytorch as pl
10
+ import sys
11
+ from pathlib import Path
12
+ from lightning.pytorch import (
13
+ LightningDataModule,
14
+ LightningModule
15
+ )
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+ if str(ROOT) not in sys.path:
19
+ sys.path.insert(0, str(ROOT))
20
+
21
+ from onescience.utils.simplefold.utils import extras, create_folders, task_wrapper
22
+ from onescience.utils.simplefold.instantiators import instantiate_callbacks
23
+ from onescience.utils.simplefold.logging_utils import log_hyperparameters
24
+ from onescience.utils.simplefold.pylogger import RankedLogger
25
+
26
+ torch.set_float32_matmul_precision("medium")
27
+ log = RankedLogger(__name__, rank_zero_only=True)
28
+
29
+
30
+ @task_wrapper
31
+ def test(cfg):
32
+ load_ckpt_path = cfg.get("load_ckpt_path", None)
33
+ assert load_ckpt_path != None
34
+
35
+ log.info(f"Instantiating model <{cfg.model._target_}>")
36
+ model: LightningModule = hydra.utils.instantiate(cfg.model)
37
+
38
+ checkpoint = torch.load(load_ckpt_path, map_location="cpu", weights_only=False)
39
+
40
+ # TODO: add load FSDP checkpoint
41
+ try:
42
+ # checkpoint in our official release only contains weights of EMA model
43
+ # therefore, we by default load EMA model weight to LightningModule for inference
44
+ for key in model.model_ema.state_dict().keys():
45
+ if key.startswith("module."):
46
+ model.model_ema.state_dict()[key].copy_(
47
+ checkpoint[key.replace("module.", "")]
48
+ )
49
+ print("Loaded weights of EMA model successfully.")
50
+ except:
51
+ # if using checkpoint from your own training, load weights of LightningModule directly
52
+ model.load_state_dict(
53
+ checkpoint["state_dict"], strict=False
54
+ )
55
+ print("Loaded weights of LightningModule successfully.")
56
+
57
+ # reset ESM model to avoid issues in loading FSDP checkpoint
58
+ model.reset_esm(cfg.model.esm_model)
59
+
60
+ seed = cfg.get("seed", 42)
61
+ pl.seed_everything(seed, workers=True)
62
+
63
+ log.info(f"Instantiating datamodule <{cfg.data._target_}>")
64
+ datamodule: LightningDataModule = hydra.utils.instantiate(cfg.data)
65
+
66
+ log.info("Instantiating callbacks...")
67
+ callbacks = instantiate_callbacks(cfg.get("callbacks"))
68
+
69
+ log.info(f"Instantiating trainer <{cfg.trainer._target_}>")
70
+ trainer = hydra.utils.instantiate(
71
+ cfg.trainer, callbacks=callbacks, logger=[], plugins=[]
72
+ )
73
+
74
+ object_dict = {
75
+ "cfg": cfg,
76
+ "datamodule": datamodule,
77
+ "model": model,
78
+ "callbacks": callbacks,
79
+ "logger": [],
80
+ "trainer": trainer,
81
+ }
82
+
83
+ if log:
84
+ log.info("Logging hyperparameters!")
85
+ log_hyperparameters(object_dict)
86
+
87
+ log.info("Starting evaluation!")
88
+ trainer.predict(model=model, datamodule=datamodule, ckpt_path=None)
89
+
90
+
91
+ @hydra.main(version_base="1.3", config_path="../config", config_name="base_eval.yaml")
92
+ def submit_run(cfg):
93
+ OmegaConf.resolve(cfg)
94
+ extras(cfg)
95
+ create_folders(cfg)
96
+ test(cfg)
97
+ return
98
+
99
+
100
+ if __name__ == "__main__":
101
+ submit_run()
scripts/inference.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import os
7
+ import sys
8
+ import torch
9
+ import hydra
10
+ import omegaconf
11
+ import argparse
12
+ import numpy as np
13
+ from copy import deepcopy
14
+ from pathlib import Path
15
+ from itertools import starmap
16
+ import lightning.pytorch as pl
17
+
18
+ ROOT = Path(__file__).resolve().parents[1]
19
+ CONFIG_DIR = ROOT / "config"
20
+ WEIGHT_DIR = ROOT / "weight"
21
+
22
+ if str(ROOT) not in sys.path:
23
+ sys.path.insert(0, str(ROOT))
24
+
25
+ from models.simplefold.flow import LinearPath
26
+ from models.simplefold.torch.sampler import EMSampler
27
+
28
+ from onescience.datapipes.simplefold.processor.protein_processor import ProteinDataProcessor
29
+ from onescience.utils.simplefold.datamodule_utils import process_one_inference_structure
30
+ from onescience.utils.simplefold.esm_utils import _af2_to_esm, esm_registry
31
+ import models.esm.pretrained as esm_pretrained
32
+ from onescience.utils.simplefold.boltz_utils import process_structure, save_structure
33
+ from onescience.utils.simplefold.fasta_utils import process_fastas, check_fasta_inputs
34
+ from onescience.datapipes.boltz_data_pipeline.feature.featurizer import BoltzFeaturizer
35
+ from onescience.datapipes.boltz_data_pipeline.tokenize.boltz_protein import BoltzTokenizer
36
+
37
+ try:
38
+ import mlx.core as mx
39
+ from mlx.utils import tree_unflatten, tree_flatten
40
+ from models.simplefold.mlx.sampler import EMSampler as EMSamplerMLX
41
+ from models.simplefold.mlx.esm_network import ESM2 as ESM2MLX
42
+ from onescience.utils.simplefold.mlx_utils import map_torch_to_mlx, map_plddt_torch_to_mlx
43
+ MLX_AVAILABLE = True
44
+ except:
45
+ MLX_AVAILABLE = False
46
+ print("MLX not installed, skip importing MLX related packages.")
47
+
48
+
49
+ SUPPORTED_MODELS = {
50
+ "simplefold_100M",
51
+ "simplefold_360M",
52
+ "simplefold_700M",
53
+ "simplefold_1.1B",
54
+ "simplefold_1.6B",
55
+ "simplefold_3B",
56
+ }
57
+
58
+ ESM2_MODEL_NAME = "esm2_t36_3B_UR50D"
59
+ MIN_REAL_WEIGHT_BYTES = 1024
60
+
61
+
62
+ def _resolve_path(path: str | os.PathLike[str], base: Path = ROOT) -> Path:
63
+ path = Path(path)
64
+ return path if path.is_absolute() else base / path
65
+
66
+
67
+ def _require_local_file(path: Path, description: str, min_bytes: int = MIN_REAL_WEIGHT_BYTES) -> Path:
68
+ if not path.exists():
69
+ raise FileNotFoundError(
70
+ f"Missing {description}: {path}\n"
71
+ f"Please place the file under {WEIGHT_DIR} or pass the corresponding environment/CLI path."
72
+ )
73
+ if path.stat().st_size < min_bytes:
74
+ raise RuntimeError(
75
+ f"{description} looks like a link/placeholder rather than a real weight file: {path}\n"
76
+ "Replace it with the real file before running inference."
77
+ )
78
+ return path
79
+
80
+
81
+ def load_local_or_hub_esm2_3b():
82
+ candidate_paths = []
83
+
84
+ esm_model_path = os.getenv("SIMPLEFOLD_ESM2_MODEL_PATH")
85
+ if esm_model_path:
86
+ candidate_paths.append(esm_model_path)
87
+
88
+ candidate_paths.extend(
89
+ [
90
+ WEIGHT_DIR / "esm_models" / f"{ESM2_MODEL_NAME}.pt",
91
+ WEIGHT_DIR / f"{ESM2_MODEL_NAME}.pt",
92
+ ]
93
+ )
94
+
95
+ seen_paths = set()
96
+ for candidate_path in candidate_paths:
97
+ if candidate_path in seen_paths:
98
+ continue
99
+ seen_paths.add(candidate_path)
100
+
101
+ candidate_path = Path(candidate_path)
102
+ regression_path = candidate_path.with_name(
103
+ f"{candidate_path.stem}-contact-regression.pt"
104
+ )
105
+ if candidate_path.exists() and regression_path.exists():
106
+ _require_local_file(candidate_path, "ESM-2 3B model weight")
107
+ _require_local_file(regression_path, "ESM-2 contact regression weight")
108
+ print(f"Loading ESM-2 3B weights from local path: {candidate_path}")
109
+ return esm_pretrained.load_model_and_alphabet(str(candidate_path))
110
+
111
+ raise FileNotFoundError(
112
+ "Local ESM-2 3B weights were not found. Put both files here:\n"
113
+ f" {WEIGHT_DIR / 'esm_models' / f'{ESM2_MODEL_NAME}.pt'}\n"
114
+ f" {WEIGHT_DIR / 'esm_models' / f'{ESM2_MODEL_NAME}-contact-regression.pt'}\n"
115
+ "or set SIMPLEFOLD_ESM2_MODEL_PATH to the .pt file. Remote download is disabled in this package."
116
+ )
117
+
118
+
119
+ def resolve_ccd_path(cache: Path) -> Path:
120
+ candidate_paths = []
121
+
122
+ ccd_path = os.getenv("SIMPLEFOLD_CCD_PATH")
123
+ if ccd_path:
124
+ candidate_paths.append(Path(ccd_path))
125
+
126
+ candidate_paths.append(WEIGHT_DIR / "ccd.pkl")
127
+
128
+ seen_paths = set()
129
+ for candidate_path in candidate_paths:
130
+ resolved_path = str(candidate_path)
131
+ if resolved_path in seen_paths:
132
+ continue
133
+ seen_paths.add(resolved_path)
134
+
135
+ if candidate_path.exists():
136
+ _require_local_file(candidate_path, "CCD dictionary")
137
+ print(f"Using local CCD dictionary: {candidate_path}")
138
+ return candidate_path
139
+
140
+ raise FileNotFoundError(
141
+ f"Missing CCD dictionary. Put ccd.pkl under {WEIGHT_DIR} or set SIMPLEFOLD_CCD_PATH."
142
+ )
143
+
144
+
145
+ def get_config_path(relative_path):
146
+ """Resolve a SimpleFold config from the bundled config directory."""
147
+ config_subpath = relative_path.replace("configs/", "")
148
+ config_path = CONFIG_DIR / config_subpath
149
+ if not config_path.is_file():
150
+ raise FileNotFoundError(f"Could not find config file: {config_path}")
151
+ return str(config_path)
152
+
153
+
154
+
155
+ def initialize_folding_model(args):
156
+ # define folding model
157
+ simplefold_model = args.simplefold_model
158
+ if simplefold_model not in SUPPORTED_MODELS:
159
+ raise ValueError(f"Unsupported model {simplefold_model!r}. Choose one of {sorted(SUPPORTED_MODELS)}")
160
+
161
+ # create checkpoint directory
162
+ ckpt_dir = _resolve_path(args.ckpt_dir)
163
+ ckpt_path = os.path.join(ckpt_dir, f"{simplefold_model}.ckpt")
164
+
165
+ # create folding model
166
+ ckpt_path = os.path.join(ckpt_dir, f"{simplefold_model}.ckpt")
167
+ _require_local_file(Path(ckpt_path), f"{simplefold_model} checkpoint")
168
+ cfg_path = get_config_path(f"configs/model/architecture/foldingdit_{simplefold_model[11:]}.yaml")
169
+
170
+ checkpoint = torch.load(ckpt_path, map_location="cpu", weights_only=False)
171
+
172
+ # load model checkpoint
173
+ if args.backend == 'torch':
174
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
175
+ model_config = omegaconf.OmegaConf.load(cfg_path)
176
+ model = hydra.utils.instantiate(model_config)
177
+ model.load_state_dict(checkpoint, strict=True)
178
+ model = model.to(device)
179
+ elif args.backend == 'mlx':
180
+ device = "cpu"
181
+ # replace torch implementations with mlx
182
+ with open(cfg_path, "r") as f:
183
+ yaml_str = f.read()
184
+ yaml_str = yaml_str.replace('torch', 'mlx')
185
+
186
+ model_config = omegaconf.OmegaConf.create(yaml_str)
187
+ model = hydra.utils.instantiate(model_config)
188
+ mlx_state_dict = {k: mx.array(v) for k, v in starmap(map_torch_to_mlx, checkpoint.items()) if k is not None}
189
+ model.update(tree_unflatten(list(mlx_state_dict.items())))
190
+ print(f"Folding model {simplefold_model} loaded.")
191
+ print(f"Using device: {device}.")
192
+
193
+ model.eval()
194
+ return model, device
195
+
196
+
197
+ def initialize_plddt_module(args, device):
198
+ if not args.plddt:
199
+ return None, None
200
+
201
+ # load pLDDT module if specified
202
+ ckpt_dir = _resolve_path(args.ckpt_dir)
203
+ plddt_ckpt_path = ckpt_dir / "plddt.ckpt"
204
+ if not plddt_ckpt_path.exists():
205
+ plddt_ckpt_path = ckpt_dir / "plddt_module_1.6B.ckpt"
206
+ _require_local_file(plddt_ckpt_path, "pLDDT checkpoint")
207
+
208
+ plddt_module_path = get_config_path("configs/model/architecture/plddt_module.yaml")
209
+ plddt_checkpoint = torch.load(plddt_ckpt_path, map_location="cpu", weights_only=False)
210
+
211
+ if args.backend == "torch":
212
+ plddt_config = omegaconf.OmegaConf.load(plddt_module_path)
213
+ plddt_out_module = hydra.utils.instantiate(plddt_config)
214
+ plddt_out_module.load_state_dict(plddt_checkpoint, strict=True)
215
+ plddt_out_module = plddt_out_module.to(device)
216
+ elif args.backend == "mlx":
217
+ # replace torch implementations with mlx
218
+ with open(plddt_module_path, "r") as f:
219
+ yaml_str = f.read()
220
+ yaml_str = yaml_str.replace('torch', 'mlx')
221
+
222
+ plddt_config = omegaconf.OmegaConf.create(yaml_str)
223
+ plddt_out_module = hydra.utils.instantiate(plddt_config)
224
+
225
+ mlx_state_dict = {k: mx.array(v) for k, v in starmap(map_plddt_torch_to_mlx, plddt_checkpoint.items()) if k is not None}
226
+ plddt_out_module.update(tree_unflatten(list(mlx_state_dict.items())))
227
+
228
+ plddt_out_module.eval()
229
+ print(f"pLDDT output module loaded with {args.backend} backend.")
230
+
231
+ plddt_latent_ckpt_path = ckpt_dir / "simplefold_1.6B.ckpt"
232
+ _require_local_file(plddt_latent_ckpt_path, "SimpleFold 1.6B pLDDT latent checkpoint")
233
+
234
+ plddt_latent_config_path = get_config_path("configs/model/architecture/foldingdit_1.6B.yaml")
235
+ plddt_latent_checkpoint = torch.load(plddt_latent_ckpt_path, map_location="cpu", weights_only=False)
236
+
237
+ if args.backend == "torch":
238
+ plddt_latent_config = omegaconf.OmegaConf.load(plddt_latent_config_path)
239
+ plddt_latent_module = hydra.utils.instantiate(plddt_latent_config)
240
+ plddt_latent_module.load_state_dict(plddt_latent_checkpoint, strict=True)
241
+ plddt_latent_module = plddt_latent_module.to(device)
242
+ elif args.backend == "mlx":
243
+ # replace torch implementations with mlx
244
+ with open(plddt_latent_config_path, "r") as f:
245
+ yaml_str = f.read()
246
+ yaml_str = yaml_str.replace('torch', 'mlx')
247
+
248
+ plddt_latent_config = omegaconf.OmegaConf.create(yaml_str)
249
+ plddt_latent_module = hydra.utils.instantiate(plddt_latent_config)
250
+ mlx_state_dict = {k: mx.array(v) for k, v in starmap(map_torch_to_mlx, plddt_latent_checkpoint.items()) if k is not None}
251
+ plddt_latent_module.update(tree_unflatten(list(mlx_state_dict.items())))
252
+
253
+ plddt_latent_module.eval()
254
+ print(f"pLDDT latent module loaded with {args.backend} backend.")
255
+
256
+ return plddt_latent_module, plddt_out_module
257
+
258
+
259
+ def initialize_esm_model(args, device):
260
+ # load ESM2 model
261
+ esm_model, esm_dict = load_local_or_hub_esm2_3b()
262
+ af2_to_esm = _af2_to_esm(esm_dict)
263
+
264
+ if args.backend == 'torch':
265
+ esm_model = esm_model.to(device)
266
+ af2_to_esm = af2_to_esm.to(device)
267
+ elif args.backend == 'mlx':
268
+ esm_model_mlx = ESM2MLX(num_layers=36, embed_dim=2560, attention_heads=40)
269
+ esm_state_dict_torch = esm_model.cpu().state_dict()
270
+
271
+ esm_state_dict_torch = {k: mx.array(v) for k, v in starmap(map_torch_to_mlx, esm_state_dict_torch.items()) if k is not None}
272
+ esm_model_mlx.update(tree_unflatten(list(esm_state_dict_torch.items())))
273
+ esm_model = esm_model_mlx
274
+ print(f"pLM ESM-3B loaded with {args.backend} backend.")
275
+
276
+ esm_model.eval()
277
+ return esm_model, esm_dict, af2_to_esm
278
+
279
+
280
+ def initialize_others(args, device):
281
+ # prepare data tokenizer, featurizer, and processor
282
+ tokenizer = BoltzTokenizer()
283
+ featurizer = BoltzFeaturizer()
284
+ processor = ProteinDataProcessor(
285
+ device=device,
286
+ scale=16.0,
287
+ ref_scale=5.0,
288
+ multiplicity=1,
289
+ inference_multiplicity=args.nsample_per_protein,
290
+ backend=args.backend,
291
+ )
292
+
293
+ # define flow process and sampler
294
+ flow = LinearPath()
295
+
296
+ if args.backend == "torch":
297
+ sampler_cls = EMSampler
298
+ elif args.backend == "mlx":
299
+ sampler_cls = EMSamplerMLX
300
+
301
+ sampler = sampler_cls(
302
+ num_timesteps=args.num_steps,
303
+ t_start=1e-4,
304
+ tau=args.tau,
305
+ log_timesteps=True,
306
+ w_cutoff=0.99,
307
+ )
308
+ return tokenizer, featurizer, processor, flow, sampler
309
+
310
+
311
+ def generate_structure(
312
+ args, batch, sampler, flow, processor,
313
+ model, plddt_latent_module, plddt_out_module, device
314
+ ):
315
+ # run inference for target protein
316
+ if args.backend == "torch":
317
+ noise = torch.randn_like(batch['coords']).to(device)
318
+ elif args.backend == "mlx":
319
+ noise = mx.random.normal(batch['coords'].shape)
320
+ out_dict = sampler.sample(model, flow, noise, batch)
321
+
322
+ if args.plddt:
323
+ if args.backend == "torch":
324
+ t = torch.ones(batch['coords'].shape[0], device=device)
325
+ # use unscaled coords to extract latent for pLDDT prediction
326
+ out_feat = plddt_latent_module(
327
+ out_dict["denoised_coords"].detach(), t, batch)
328
+ plddt_out_dict = plddt_out_module(
329
+ out_feat["latent"].detach(),
330
+ batch,
331
+ )
332
+ elif args.backend == "mlx":
333
+ t = mx.ones(batch['coords'].shape[0])
334
+ # use unscaled coords to extract latent for pLDDT prediction
335
+ out_feat = plddt_latent_module(
336
+ out_dict["denoised_coords"], t, batch)
337
+ plddt_out_dict = plddt_out_module(
338
+ out_feat["latent"],
339
+ batch,
340
+ )
341
+ # scale pLDDT to [0, 100]
342
+ plddts = plddt_out_dict["plddt"] * 100.0
343
+ else:
344
+ plddts = None
345
+
346
+ out_dict = processor.postprocess(out_dict, batch)
347
+ # sampled_coord = out_dict['denoised_coords'].detach()
348
+ if args.backend == "torch":
349
+ sampled_coord = out_dict['denoised_coords'].detach()
350
+ else:
351
+ sampled_coord = out_dict['denoised_coords']
352
+
353
+ pad_mask = batch['atom_pad_mask']
354
+ return sampled_coord, pad_mask, plddts
355
+
356
+
357
+ def predict_structures_from_fastas(args):
358
+ # create output directories
359
+ output_dir = _resolve_path(args.output_dir)
360
+ output_dir.mkdir(parents=True, exist_ok=True)
361
+ prediction_dir = output_dir / f"predictions_{args.simplefold_model}"
362
+ prediction_dir.mkdir(parents=True, exist_ok=True)
363
+ cache = output_dir / "cache"
364
+ cache.mkdir(parents=True, exist_ok=True)
365
+
366
+ # set random seed for reproducibility
367
+ pl.seed_everything(args.seed, workers=True)
368
+
369
+ if args.backend == "mlx" and not MLX_AVAILABLE:
370
+ args.backend = "torch"
371
+ print("MLX not available, switch to torch backend.")
372
+
373
+ # initialize models
374
+ model, device = initialize_folding_model(args)
375
+ plddt_latent_module, plddt_out_module = initialize_plddt_module(args, device)
376
+ esm_model, esm_dict, af2_to_esm = initialize_esm_model(args, device)
377
+
378
+ # initialize other components
379
+ tokenizer, featurizer, processor, flow, sampler = initialize_others(args, device)
380
+
381
+ # process fasta files to input format
382
+ ccd_path = resolve_ccd_path(cache)
383
+ data = check_fasta_inputs(_resolve_path(args.fasta_path))
384
+ if not data:
385
+ raise ValueError("No valid input files found. Please check the input directory.")
386
+ process_fastas(
387
+ data=data,
388
+ out_dir=output_dir,
389
+ ccd_path=ccd_path,
390
+ )
391
+
392
+ for struct_file in output_dir.glob("structures/*.npz"):
393
+ record_file = output_dir / "records" / f"{struct_file.stem}.json"
394
+
395
+ # prepare the target protein data for inference
396
+ batch, structure, record = process_one_inference_structure(
397
+ struct_file, record_file,
398
+ tokenizer, featurizer, processor,
399
+ esm_model, esm_dict, af2_to_esm,
400
+ )
401
+
402
+ sampled_coord, pad_mask, plddts = generate_structure(
403
+ args, batch, sampler, flow, processor,
404
+ model, plddt_latent_module, plddt_out_module, device
405
+ )
406
+
407
+ for i in range(args.nsample_per_protein):
408
+ sampled_coord_i = sampled_coord[i]
409
+ pad_mask_i = pad_mask[i]
410
+
411
+ # save the generated structure
412
+ structure_save = process_structure(
413
+ deepcopy(structure), sampled_coord_i, pad_mask_i, record, backend=args.backend
414
+ )
415
+ outname = f"{record.id}_sampled_{i}"
416
+ save_structure(
417
+ structure_save, prediction_dir, outname,
418
+ output_format=args.output_format,
419
+ plddts=plddts[i] if plddts is not None else None
420
+ )
scripts/preflight.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Preflight checks for the packaged SimpleFold ModelScope layout."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ if str(ROOT) not in sys.path:
13
+ sys.path.insert(0, str(ROOT))
14
+
15
+ REQUIRED_FILES = [
16
+ "README.md",
17
+ "configuration.json",
18
+ "requirements_dcu.txt",
19
+ "env_install.sh",
20
+ "config/train.yaml",
21
+ "config/base_train.yaml",
22
+ "config/base_eval.yaml",
23
+ "config/model/architecture/foldingdit_100M.yaml",
24
+ "config/model/architecture/foldingdit_1.6B.yaml",
25
+ "config/model/architecture/plddt_module.yaml",
26
+ "scripts/run_inference.py",
27
+ "scripts/inference.py",
28
+ "scripts/train.py",
29
+ "scripts/train_fsdp.py",
30
+ "scripts/evaluate.py",
31
+ "scripts/process_data.py",
32
+ "scripts/tokenize_data.py",
33
+ "examples/minimal.fasta",
34
+ "modules/models/simplefold/flow.py",
35
+ "modules/datapipes/simplefold/processor/protein_processor.py",
36
+ "modules/utils/simplefold/esm_utils.py",
37
+ "modules/datapipes/boltz_data_pipeline/types.py",
38
+ "modules/models/esm/pretrained.py",
39
+ ]
40
+
41
+ REQUIRED_WEIGHTS = [
42
+ "simplefold_100M.ckpt",
43
+ "simplefold_360M.ckpt",
44
+ "simplefold_700M.ckpt",
45
+ "simplefold_1.1B.ckpt",
46
+ "simplefold_1.6B.ckpt",
47
+ "simplefold_3B.ckpt",
48
+ "plddt.ckpt",
49
+ "plddt_module_1.6B.ckpt",
50
+ "ccd.pkl",
51
+ "boltz1_conf.ckpt",
52
+ ]
53
+
54
+ OPTIONAL_ESM_WEIGHTS = [
55
+ "esm_models/esm2_t36_3B_UR50D.pt",
56
+ "esm_models/esm2_t36_3B_UR50D-contact-regression.pt",
57
+ ]
58
+
59
+
60
+ def ok(message: str) -> None:
61
+ print(f"[OK] {message}")
62
+
63
+
64
+ def warn(message: str) -> None:
65
+ print(f"[WARN] {message}")
66
+
67
+
68
+ def fail(message: str) -> None:
69
+ print(f"[FAIL] {message}")
70
+ raise SystemExit(1)
71
+
72
+
73
+ def check_files() -> None:
74
+ missing = [path for path in REQUIRED_FILES if not (ROOT / path).exists()]
75
+ if missing:
76
+ fail("Missing required files: " + ", ".join(missing))
77
+ ok("Required package files are present")
78
+
79
+
80
+ def check_weights(strict: bool) -> None:
81
+ missing = []
82
+ placeholders = []
83
+ for name in REQUIRED_WEIGHTS:
84
+ path = ROOT / "weight" / name
85
+ if not path.exists():
86
+ missing.append(name)
87
+ elif path.stat().st_size < 1024:
88
+ placeholders.append(name)
89
+ if missing:
90
+ fail("Missing weight files: " + ", ".join(missing))
91
+ if placeholders:
92
+ message = (
93
+ "These files look like link/placeholder files and must be replaced "
94
+ "before real inference/training: " + ", ".join(placeholders)
95
+ )
96
+ if strict:
97
+ fail(message)
98
+ warn(message)
99
+ else:
100
+ ok("Required SimpleFold weights look like real files")
101
+
102
+ missing_esm = [name for name in OPTIONAL_ESM_WEIGHTS if not (ROOT / "weight" / name).exists()]
103
+ if missing_esm:
104
+ warn(
105
+ "ESM-2 3B local weights are not present. Put them under weight/esm_models "
106
+ "before running inference: " + ", ".join(missing_esm)
107
+ )
108
+ else:
109
+ ok("Local ESM-2 3B files are present")
110
+
111
+
112
+ def check_imports(strict: bool) -> None:
113
+ modules = [
114
+ "modules.models.simplefold.flow",
115
+ "modules.models.simplefold.torch.architecture",
116
+ "modules.datapipes.simplefold.processor.protein_processor",
117
+ "modules.utils.simplefold.esm_utils",
118
+ "modules.datapipes.boltz_data_pipeline.types",
119
+ "modules.models.esm.pretrained",
120
+ ]
121
+ missing_dependencies = []
122
+ for module_name in modules:
123
+ try:
124
+ importlib.import_module(module_name)
125
+ except ModuleNotFoundError as exc:
126
+ missing_dependencies.append(f"{module_name}: {exc.name}")
127
+ if missing_dependencies:
128
+ message = (
129
+ "Some imports need external Python dependencies. Install requirements_dcu.txt "
130
+ "and rerun with --strict-imports for a full check: "
131
+ + "; ".join(missing_dependencies)
132
+ )
133
+ if strict:
134
+ fail(message)
135
+ warn(message)
136
+ else:
137
+ ok("Bundled Python modules import successfully")
138
+
139
+
140
+ def main() -> None:
141
+ parser = argparse.ArgumentParser(description="Check packaged SimpleFold runtime files.")
142
+ parser.add_argument("--strict-weights", action="store_true")
143
+ parser.add_argument("--strict-imports", action="store_true")
144
+ args = parser.parse_args()
145
+
146
+ check_files()
147
+ check_weights(strict=args.strict_weights)
148
+ check_imports(strict=args.strict_imports)
149
+ ok("SimpleFold package preflight finished")
150
+
151
+
152
+ if __name__ == "__main__":
153
+ main()
scripts/process_data.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ #
3
+ # For licensing see accompanying LICENSE file.
4
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
5
+ #
6
+
7
+ import argparse
8
+ import multiprocessing
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ if str(ROOT) not in sys.path:
14
+ sys.path.insert(0, str(ROOT))
15
+
16
+ from modules.datapipes.simplefold.process_mmcif import process
17
+
18
+
19
+ if __name__ == "__main__":
20
+ parser = argparse.ArgumentParser(description="Process MMCIF data.")
21
+ parser.add_argument(
22
+ "--data_dir",
23
+ type=Path,
24
+ required=True,
25
+ help="The directory containing the MMCIF files.",
26
+ )
27
+ parser.add_argument(
28
+ "--out_dir",
29
+ type=Path,
30
+ default="data",
31
+ help="The output directory.",
32
+ )
33
+ parser.add_argument(
34
+ "--num-processes",
35
+ type=int,
36
+ default=multiprocessing.cpu_count(),
37
+ help="The number of processes.",
38
+ )
39
+ parser.add_argument(
40
+ "--redis-host",
41
+ type=str,
42
+ default="localhost",
43
+ help="The Redis host, only used with --use-redis.",
44
+ )
45
+ parser.add_argument(
46
+ "--redis-port",
47
+ type=int,
48
+ default=7777,
49
+ help="The Redis port, only used with --use-redis.",
50
+ )
51
+ parser.add_argument(
52
+ "--use-redis",
53
+ action="store_true",
54
+ help="Use an external Redis CCD resource instead of local weight/ccd.pkl.",
55
+ )
56
+ parser.add_argument(
57
+ "--ccd-path",
58
+ type=Path,
59
+ default=ROOT / "weight" / "ccd.pkl",
60
+ help="Path to the local CCD pickle file.",
61
+ )
62
+ parser.add_argument(
63
+ "--use-assembly",
64
+ action="store_true",
65
+ help="Whether to use assembly 1.",
66
+ )
67
+ parser.add_argument(
68
+ "--max-file-size",
69
+ type=int,
70
+ default=None,
71
+ )
72
+ args = parser.parse_args()
73
+ process(args)
scripts/run_inference.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Run SimpleFold structure prediction from the packaged ModelScope layout."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+ if str(ROOT) not in sys.path:
12
+ sys.path.insert(0, str(ROOT))
13
+
14
+ from inference import predict_structures_from_fastas
15
+
16
+
17
+ def build_parser() -> argparse.ArgumentParser:
18
+ parser = argparse.ArgumentParser(description="Run SimpleFold structure prediction.")
19
+ parser.add_argument("--simplefold_model", default="simplefold_100M")
20
+ parser.add_argument("--ckpt_dir", default="weight")
21
+ parser.add_argument("--output_dir", default="outputs/minimal_inference")
22
+ parser.add_argument("--num_steps", type=int, default=10)
23
+ parser.add_argument("--tau", type=float, default=0.01)
24
+ parser.add_argument("--no_log_timesteps", action="store_true")
25
+ parser.add_argument("--fasta_path", default="examples/minimal.fasta")
26
+ parser.add_argument("--nsample_per_protein", type=int, default=1)
27
+ parser.add_argument("--plddt", action="store_true")
28
+ parser.add_argument("--output_format", default="mmcif", choices=["pdb", "mmcif"])
29
+ parser.add_argument("--backend", default="torch", choices=["torch", "mlx"])
30
+ parser.add_argument("--seed", type=int, default=42)
31
+ return parser
32
+
33
+
34
+ def main() -> None:
35
+ args = build_parser().parse_args()
36
+ predict_structures_from_fastas(args)
37
+
38
+
39
+ if __name__ == "__main__":
40
+ main()
41
+
scripts/tokenize_data.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ #
3
+ # For licensing see accompanying LICENSE file.
4
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
5
+ #
6
+
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+ from tqdm import tqdm
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ if str(ROOT) not in sys.path:
14
+ sys.path.insert(0, str(ROOT))
15
+
16
+ from onescience.datapipes.boltz_data_pipeline.tokenize.boltz_protein import BoltzTokenizer
17
+ from onescience.datapipes.boltz_data_pipeline.types import Manifest
18
+ from onescience.datapipes.simplefold.process_structure import tokenize_structure, finalize
19
+
20
+
21
+ if __name__ == "__main__":
22
+ parser = argparse.ArgumentParser(description="Tokenize structure data.")
23
+ parser.add_argument(
24
+ "--target_dir",
25
+ type=str,
26
+ required=True,
27
+ help="Directory containing the processed structure data.",
28
+ )
29
+ parser.add_argument(
30
+ "--token_dir",
31
+ type=str,
32
+ required=True,
33
+ help="Directory to save the tokenized data.",
34
+ )
35
+ args = parser.parse_args()
36
+
37
+ target_dir = Path(args.target_dir)
38
+ manifest_path = target_dir / "manifest.json"
39
+ manifest: Manifest = Manifest.load(manifest_path)
40
+ tokenizer = BoltzTokenizer()
41
+ records = manifest.records
42
+ print(f"Number of records after filtering: {len(records)}")
43
+
44
+ save_token_dir = Path(args.token_dir) / "tokens"
45
+ save_token_record_dir = Path(args.token_dir) / "records"
46
+ save_token_dir.mkdir(parents=True, exist_ok=True)
47
+ save_token_record_dir.mkdir(parents=True, exist_ok=True)
48
+
49
+ for record in tqdm(records):
50
+ tokenize_structure(
51
+ record,
52
+ tokenizer,
53
+ target_dir,
54
+ str(save_token_dir),
55
+ save_token_record_dir,
56
+ )
57
+
58
+ finalize(Path(args.token_dir))
scripts/train.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import torch
7
+ import lightning.pytorch as pl
8
+ from lightning.pytorch import LightningDataModule, LightningModule
9
+ import hydra
10
+ from omegaconf import OmegaConf
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ if str(ROOT) not in sys.path:
17
+ sys.path.insert(0, str(ROOT))
18
+
19
+ from onescience.utils.simplefold.utils import (
20
+ extras,
21
+ create_folders,
22
+ task_wrapper,
23
+ )
24
+ from onescience.utils.simplefold.instantiators import (
25
+ instantiate_callbacks,
26
+ instantiate_loggers,
27
+ instantiate_trainer,
28
+ )
29
+ from onescience.utils.simplefold.logging_utils import log_hyperparameters
30
+ from onescience.utils.simplefold.pylogger import RankedLogger
31
+
32
+ log = RankedLogger(__name__, rank_zero_only=True)
33
+
34
+ ESM2_MODEL_NAME = "esm2_t36_3B_UR50D"
35
+
36
+ torch.set_float32_matmul_precision("medium")
37
+ torch.backends.cuda.matmul.allow_tf32 = True # This flag defaults to False
38
+ torch.backends.cudnn.allow_tf32 = True # This flag defaults to True
39
+
40
+
41
+ def configure_local_esm_registry(esm_model: str | None) -> None:
42
+ if esm_model != "esm2_3B":
43
+ return
44
+
45
+ model_path = Path(
46
+ os.getenv(
47
+ "SIMPLEFOLD_ESM2_MODEL_PATH",
48
+ ROOT / "weight" / "esm_models" / f"{ESM2_MODEL_NAME}.pt",
49
+ )
50
+ )
51
+ regression_path = model_path.with_name(f"{model_path.stem}-contact-regression.pt")
52
+
53
+ if not model_path.exists() or not regression_path.exists():
54
+ raise FileNotFoundError(
55
+ "Local ESM-2 3B weights were not found. Expected files:\n"
56
+ f" {model_path}\n"
57
+ f" {regression_path}\n"
58
+ "Set SIMPLEFOLD_ESM2_MODEL_PATH to the local .pt file if it lives elsewhere."
59
+ )
60
+
61
+ import models.esm.pretrained as local_esm_pretrained
62
+ import onescience.utils.simplefold.esm_utils as esm_utils
63
+
64
+ def load_local_esm2_3b():
65
+ print(f"Loading ESM-2 3B weights from local path: {model_path}")
66
+ return local_esm_pretrained.load_model_and_alphabet(str(model_path))
67
+
68
+ esm_utils.esm_registry["esm2_3B"] = load_local_esm2_3b
69
+ simplefold_module = sys.modules.get("models.simplefold.simplefold")
70
+ if simplefold_module is not None:
71
+ simplefold_module.esm_registry["esm2_3B"] = load_local_esm2_3b
72
+
73
+
74
+ @task_wrapper
75
+ def train(cfg):
76
+ seed = cfg.get("seed", 42)
77
+ pl.seed_everything(seed, workers=True)
78
+ configure_local_esm_registry(cfg.model.get("esm_model", None))
79
+
80
+ log.info(f"Instantiating model <{cfg.model._target_}>")
81
+ model: LightningModule = hydra.utils.instantiate(cfg.model)
82
+ load_ckpt_path = cfg.get("load_ckpt_path", None)
83
+
84
+ if load_ckpt_path is not None:
85
+ # load existing ckpt
86
+ log.info(f"Resuming from checkpoint <{cfg.load_ckpt_path}>...")
87
+ model.strict_loading = False
88
+
89
+ # manually reset these variables in case of fine-tuning
90
+ model.lddt_weight_schedule = cfg.model.get("lddt_weight_schedule", False)
91
+ model.plddt_training = cfg.model.get("plddt_training", False)
92
+
93
+ # reset ESM model to avoid issues in loading FSDP checkpoint
94
+ model.reset_esm(cfg.model.esm_model)
95
+
96
+ log.info(f"Instantiating datamodule <{cfg.data._target_}>")
97
+ datamodule: LightningDataModule = hydra.utils.instantiate(cfg.data)
98
+
99
+ log.info("Instantiating callbacks...")
100
+ callbacks = instantiate_callbacks(cfg.get("callbacks"))
101
+
102
+ log.info("Instantiating loggers...")
103
+ OmegaConf.set_struct(cfg.logger, True)
104
+ loggers = instantiate_loggers(cfg.get("logger"))
105
+
106
+ log.info(f"Instantiating trainer <{cfg.trainer._target_}>")
107
+ trainer = instantiate_trainer(
108
+ cfg.trainer, callbacks=callbacks, logger=loggers, plugins=None
109
+ )
110
+
111
+ object_dict = {
112
+ "cfg": cfg,
113
+ "datamodule": datamodule,
114
+ "model": model,
115
+ "callbacks": callbacks,
116
+ "logger": loggers,
117
+ "trainer": trainer,
118
+ }
119
+
120
+ if log:
121
+ log.info("Logging hyperparameters!")
122
+ log_hyperparameters(object_dict)
123
+
124
+ log.info("Starting training!")
125
+ trainer.fit(
126
+ model=model,
127
+ datamodule=datamodule,
128
+ ckpt_path=load_ckpt_path,
129
+ )
130
+
131
+
132
+ @hydra.main(version_base="1.3", config_path="../config", config_name="train.yaml")
133
+ def submit_run(cfg):
134
+ OmegaConf.resolve(cfg)
135
+ extras(cfg)
136
+ create_folders(cfg)
137
+ train(cfg)
138
+ return
139
+
140
+
141
+ if __name__ == "__main__":
142
+ submit_run()
scripts/train_fsdp.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # For licensing see accompanying LICENSE file.
3
+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
4
+ #
5
+
6
+ import os
7
+ import hydra
8
+ import torch
9
+ import functools
10
+ from omegaconf import OmegaConf
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ if str(ROOT) not in sys.path:
16
+ sys.path.insert(0, str(ROOT))
17
+
18
+ import lightning.pytorch as pl
19
+ from lightning.pytorch import LightningDataModule, LightningModule
20
+ from lightning.pytorch.strategies import FSDPStrategy
21
+ from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy
22
+
23
+ from models.simplefold.torch.blocks import DiTBlock
24
+ from onescience.utils.simplefold.utils import (
25
+ extras,
26
+ create_folders,
27
+ task_wrapper,
28
+ )
29
+ from onescience.utils.simplefold.instantiators import (
30
+ instantiate_callbacks,
31
+ instantiate_loggers,
32
+ )
33
+ from onescience.utils.simplefold.logging_utils import log_hyperparameters
34
+ from onescience.utils.simplefold.pylogger import RankedLogger
35
+
36
+ log = RankedLogger(__name__, rank_zero_only=True)
37
+ ESM2_MODEL_NAME = "esm2_t36_3B_UR50D"
38
+ torch.set_float32_matmul_precision("medium")
39
+
40
+
41
+ def configure_local_esm_registry(esm_model: str | None) -> None:
42
+ if esm_model != "esm2_3B":
43
+ return
44
+
45
+ model_path = Path(
46
+ os.getenv(
47
+ "SIMPLEFOLD_ESM2_MODEL_PATH",
48
+ ROOT / "weight" / "esm_models" / f"{ESM2_MODEL_NAME}.pt",
49
+ )
50
+ )
51
+ regression_path = model_path.with_name(f"{model_path.stem}-contact-regression.pt")
52
+
53
+ if not model_path.exists() or not regression_path.exists():
54
+ raise FileNotFoundError(
55
+ "Local ESM-2 3B weights were not found. Expected files:\n"
56
+ f" {model_path}\n"
57
+ f" {regression_path}\n"
58
+ "Set SIMPLEFOLD_ESM2_MODEL_PATH to the local .pt file if it lives elsewhere."
59
+ )
60
+
61
+ import models.esm.pretrained as local_esm_pretrained
62
+ import onescience.utils.simplefold.esm_utils as esm_utils
63
+
64
+ def load_local_esm2_3b():
65
+ print(f"Loading ESM-2 3B weights from local path: {model_path}")
66
+ return local_esm_pretrained.load_model_and_alphabet(str(model_path))
67
+
68
+ esm_utils.esm_registry["esm2_3B"] = load_local_esm2_3b
69
+ simplefold_module = sys.modules.get("models.simplefold.simplefold")
70
+ if simplefold_module is not None:
71
+ simplefold_module.esm_registry["esm2_3B"] = load_local_esm2_3b
72
+
73
+
74
+ @task_wrapper
75
+ def train(cfg):
76
+ seed = cfg.get("seed", 42)
77
+ pl.seed_everything(seed, workers=True)
78
+ configure_local_esm_registry(cfg.model.get("esm_model", None))
79
+
80
+ log.info(f"Instantiating model <{cfg.model._target_}>")
81
+ model: LightningModule = hydra.utils.instantiate(cfg.model)
82
+ load_ckpt_path = cfg.get("load_ckpt_path", None)
83
+
84
+ if load_ckpt_path is not None:
85
+ # load existing ckpt
86
+ log.info(f"Resuming from checkpoint <{cfg.load_ckpt_path}>...")
87
+ model.strict_loading = False
88
+
89
+ # manually reset these variables in case of fine-tuning
90
+ model.lddt_weight_schedule = cfg.model.get("lddt_weight_schedule", False)
91
+ model.plddt_training = cfg.model.get("plddt_training", False)
92
+
93
+ log.info(f"Instantiating datamodule <{cfg.data._target_}>")
94
+ datamodule: LightningDataModule = hydra.utils.instantiate(cfg.data)
95
+
96
+ log.info("Instantiating callbacks...")
97
+ callbacks = instantiate_callbacks(cfg.get("callbacks"))
98
+
99
+ log.info("Instantiating loggers...")
100
+ OmegaConf.set_struct(cfg.logger, True)
101
+ loggers = instantiate_loggers(cfg.get("logger"))
102
+
103
+ # When using FSDP, we need to manually specify the wrap policy
104
+ # and activation checkpointing policy for transformer layers.
105
+
106
+ log.info(f"Instantiating trainer <{cfg.trainer._target_}>")
107
+
108
+ esm_layer_class = model.esm_model.layers[0].__class__
109
+
110
+ transformer_auto_wrapper_policy = functools.partial(
111
+ transformer_auto_wrap_policy,
112
+ transformer_layer_cls={DiTBlock, esm_layer_class},
113
+ )
114
+ strategy = FSDPStrategy(
115
+ auto_wrap_policy=transformer_auto_wrapper_policy,
116
+ activation_checkpointing_policy={DiTBlock, esm_layer_class},
117
+ use_orig_params=True,
118
+ state_dict_type="sharded",
119
+ limit_all_gathers=True,
120
+ cpu_offload=False
121
+ )
122
+ trainer = hydra.utils.instantiate(
123
+ cfg.trainer,
124
+ strategy=strategy,
125
+ callbacks=callbacks,
126
+ logger=loggers,
127
+ plugins=None
128
+ )
129
+
130
+ object_dict = {
131
+ "cfg": cfg,
132
+ "datamodule": datamodule,
133
+ "model": model,
134
+ "callbacks": callbacks,
135
+ "logger": loggers,
136
+ "trainer": trainer,
137
+ }
138
+
139
+ if log:
140
+ log.info("Logging hyperparameters!")
141
+ log_hyperparameters(object_dict)
142
+
143
+ log.info("Starting training!")
144
+ trainer.fit(
145
+ model=model,
146
+ datamodule=datamodule,
147
+ ckpt_path=load_ckpt_path,
148
+ )
149
+
150
+
151
+ @hydra.main(version_base="1.3", config_path="../config", config_name="base_train.yaml")
152
+ def submit_run(cfg):
153
+ OmegaConf.resolve(cfg)
154
+ extras(cfg)
155
+ create_folders(cfg)
156
+ train(cfg)
157
+ return
158
+
159
+
160
+ if __name__ == "__main__":
161
+ submit_run()