D4CMPP2 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. D4CMPP2/_Data/AGENTS.md +24 -0
  2. D4CMPP2/_Data/Aqsoldb.csv +9291 -0
  3. D4CMPP2/_Data/BradleyMP.csv +3042 -0
  4. D4CMPP2/_Data/Lipophilicity.csv +1131 -0
  5. D4CMPP2/_Data/README.md +8 -0
  6. D4CMPP2/_Data/__init__.py +26 -0
  7. D4CMPP2/_Data/optical.csv +20237 -0
  8. D4CMPP2/_Data/test.csv +190 -0
  9. D4CMPP2/__init__.py +16 -0
  10. D4CMPP2/__main__.py +5 -0
  11. D4CMPP2/_main.py +500 -0
  12. D4CMPP2/cli.py +7 -0
  13. D4CMPP2/exceptions.py +53 -0
  14. D4CMPP2/grid_search.py +259 -0
  15. D4CMPP2/network_refer.yaml +160 -0
  16. D4CMPP2/networks/AFP_model.py +72 -0
  17. D4CMPP2/networks/AFPwithSolv_model.py +72 -0
  18. D4CMPP2/networks/DMPNN_model.py +90 -0
  19. D4CMPP2/networks/DMPNNwithSolv_model.py +89 -0
  20. D4CMPP2/networks/GAT_model.py +48 -0
  21. D4CMPP2/networks/GATwithSolv_model.py +63 -0
  22. D4CMPP2/networks/GCN_model.py +113 -0
  23. D4CMPP2/networks/GCNwithSolv_model.py +103 -0
  24. D4CMPP2/networks/GC_model.py +122 -0
  25. D4CMPP2/networks/ISATPM_model.py +14 -0
  26. D4CMPP2/networks/ISATPN_model.py +199 -0
  27. D4CMPP2/networks/ISAT_model.py +90 -0
  28. D4CMPP2/networks/MPNN_model.py +56 -0
  29. D4CMPP2/networks/MPNNwithSolv_model.py +72 -0
  30. D4CMPP2/networks/__init__.py +25 -0
  31. D4CMPP2/networks/base.py +250 -0
  32. D4CMPP2/networks/registry.py +187 -0
  33. D4CMPP2/networks/src/AFP.py +118 -0
  34. D4CMPP2/networks/src/BiDropout.py +29 -0
  35. D4CMPP2/networks/src/DMPNN.py +35 -0
  36. D4CMPP2/networks/src/GAT.py +69 -0
  37. D4CMPP2/networks/src/GC.py +85 -0
  38. D4CMPP2/networks/src/GCN.py +71 -0
  39. D4CMPP2/networks/src/ISAT.py +153 -0
  40. D4CMPP2/networks/src/Linear.py +49 -0
  41. D4CMPP2/networks/src/MPNN.py +56 -0
  42. D4CMPP2/networks/src/SolventLayer.py +62 -0
  43. D4CMPP2/networks/src/__init__.py +0 -0
  44. D4CMPP2/networks/src/distGCN.py +21 -0
  45. D4CMPP2/networks/src/pyg_hetero.py +24 -0
  46. D4CMPP2/optimize.py +472 -0
  47. D4CMPP2/src/Analyzer/ISAAnalyzer.py +458 -0
  48. D4CMPP2/src/Analyzer/ISAPNAnalyzer.py +366 -0
  49. D4CMPP2/src/Analyzer/ISAwSAnalyzer.py +117 -0
  50. D4CMPP2/src/Analyzer/MolAnalyzer.py +319 -0
  51. D4CMPP2/src/Analyzer/__init__.py +54 -0
  52. D4CMPP2/src/Analyzer/core.py +480 -0
  53. D4CMPP2/src/Analyzer/factory.py +166 -0
  54. D4CMPP2/src/Analyzer/interpretation.py +232 -0
  55. D4CMPP2/src/Analyzer/results.py +101 -0
  56. D4CMPP2/src/DataManager/Dataset/GraphDataset.py +314 -0
  57. D4CMPP2/src/DataManager/Dataset/ISAGraphDataset.py +384 -0
  58. D4CMPP2/src/DataManager/Dataset/__init__.py +0 -0
  59. D4CMPP2/src/DataManager/GraphGenerator/ISAGraphGenerator.py +223 -0
  60. D4CMPP2/src/DataManager/GraphGenerator/MolGraphGenerator.py +73 -0
  61. D4CMPP2/src/DataManager/GraphGenerator/__init__.py +14 -0
  62. D4CMPP2/src/DataManager/ISADataManager.py +67 -0
  63. D4CMPP2/src/DataManager/MolDataManager.py +735 -0
  64. D4CMPP2/src/DataManager/__init__.py +14 -0
  65. D4CMPP2/src/DataManager/contracts.py +179 -0
  66. D4CMPP2/src/NetworkManager/ISANetworkManager.py +12 -0
  67. D4CMPP2/src/NetworkManager/NetworkManager.py +520 -0
  68. D4CMPP2/src/NetworkManager/__init__.py +14 -0
  69. D4CMPP2/src/PostProcessor.py +160 -0
  70. D4CMPP2/src/TrainManager/ISATrainManager.py +26 -0
  71. D4CMPP2/src/TrainManager/TrainManager.py +254 -0
  72. D4CMPP2/src/TrainManager/__init__.py +14 -0
  73. D4CMPP2/src/TrainManager/callbacks.py +119 -0
  74. D4CMPP2/src/__init__.py +0 -0
  75. D4CMPP2/src/utils/PATH.py +246 -0
  76. D4CMPP2/src/utils/__init__.py +0 -0
  77. D4CMPP2/src/utils/argparser.py +56 -0
  78. D4CMPP2/src/utils/checkpointing.py +90 -0
  79. D4CMPP2/src/utils/config_resolution.py +123 -0
  80. D4CMPP2/src/utils/config_validation.py +370 -0
  81. D4CMPP2/src/utils/csv_validation.py +105 -0
  82. D4CMPP2/src/utils/data_quality.py +181 -0
  83. D4CMPP2/src/utils/featureizer.py +202 -0
  84. D4CMPP2/src/utils/functional_group.csv +169 -0
  85. D4CMPP2/src/utils/graph_cache.py +213 -0
  86. D4CMPP2/src/utils/leaderboard.py +212 -0
  87. D4CMPP2/src/utils/metrics.py +31 -0
  88. D4CMPP2/src/utils/module_loader.py +147 -0
  89. D4CMPP2/src/utils/output.py +80 -0
  90. D4CMPP2/src/utils/reproducibility.py +70 -0
  91. D4CMPP2/src/utils/run_manifest.py +175 -0
  92. D4CMPP2/src/utils/scaler.py +40 -0
  93. D4CMPP2/src/utils/sculptor.py +713 -0
  94. D4CMPP2/src/utils/splitting.py +250 -0
  95. D4CMPP2/src/utils/supportfile_saver.py +94 -0
  96. D4CMPP2/src/utils/tools.py +156 -0
  97. D4CMPP2/src/utils/transfer_learning.py +111 -0
  98. d4cmpp2-0.4.0.dist-info/METADATA +420 -0
  99. d4cmpp2-0.4.0.dist-info/RECORD +103 -0
  100. d4cmpp2-0.4.0.dist-info/WHEEL +5 -0
  101. d4cmpp2-0.4.0.dist-info/entry_points.txt +2 -0
  102. d4cmpp2-0.4.0.dist-info/licenses/LICENSE +21 -0
  103. d4cmpp2-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,199 @@
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from D4CMPP2.networks.base import (
5
+ Hyperparameter,
6
+ InputContract,
7
+ ISA_HYPERPARAMETERS,
8
+ ISA_OPTIMIZATION_SPACE,
9
+ MolecularNetwork,
10
+ )
11
+ from D4CMPP2.networks.src.ISAT import ISATconvolution_PM
12
+ from D4CMPP2.networks.src.Linear import Linears
13
+ from D4CMPP2.networks.src.BiDropout import Bi_Dropout
14
+ from D4CMPP2.networks.src.GCN import graph_sum_pool
15
+ from D4CMPP2.networks.src.pyg_hetero import relation_graph
16
+
17
+ class ISATPN(MolecularNetwork):
18
+ """Positive-negative ISAT with contribution-distribution regularization."""
19
+
20
+ model_name = "isatpn"
21
+ required_config = ("node_dim", "edge_dim", "target_dim")
22
+ input_contract = InputContract(
23
+ required=(
24
+ "compound_graphs",
25
+ "compound_r_node",
26
+ "compound_i_node",
27
+ "compound_r2r_edge",
28
+ "compound_d2d_edge",
29
+ ),
30
+ optional=("get_feature", "get_score"),
31
+ )
32
+ hyperparameters = {
33
+ **ISA_HYPERPARAMETERS,
34
+ "score_variance_target": Hyperparameter(
35
+ "float", default=0.17, low=0.0, search_low=0.01,
36
+ search_high=1.0, grid=(0.1, 0.17, 0.25),
37
+ description="Target variance of positive and negative scores.",
38
+ ),
39
+ "score_mean_target": Hyperparameter(
40
+ "float", default=0.5, low=0.0, high=1.0,
41
+ search_low=0.0, search_high=1.0, grid=(0.3, 0.5, 0.7),
42
+ description="Target mean of positive and negative scores.",
43
+ ),
44
+ "score_magnitude_weight": Hyperparameter(
45
+ "float", default=0.09, low=0.0, search_low=0.0,
46
+ search_high=0.2, grid=(0.0, 0.03, 0.09, 0.15),
47
+ description="Weight of score magnitude regularization.",
48
+ ),
49
+ "score_variance_weight": Hyperparameter(
50
+ "float", default=0.03, low=0.0, search_low=0.0,
51
+ search_high=0.2, grid=(0.0, 0.03, 0.1),
52
+ description="Weight of score variance regularization.",
53
+ ),
54
+ "score_mean_weight": Hyperparameter(
55
+ "float", default=0.03, low=0.0, search_low=0.0,
56
+ search_high=0.2, grid=(0.0, 0.03, 0.1),
57
+ description="Weight of score mean regularization.",
58
+ ),
59
+ "bidirectional_dropout": Hyperparameter(
60
+ "float", default=0.4, low=0.0, high=0.5,
61
+ search_low=0.0, search_high=0.5,
62
+ grid=(0.0, 0.2, 0.4),
63
+ description="Probability of suppressing either contribution branch.",
64
+ ),
65
+ }
66
+ default_optimization_space = (
67
+ *ISA_OPTIMIZATION_SPACE,
68
+ "score_magnitude_weight",
69
+ "score_variance_weight",
70
+ "score_mean_weight",
71
+ "bidirectional_dropout",
72
+ )
73
+
74
+ def __init__(self, config):
75
+ aliases = {
76
+ "score_variance_target": "score_var",
77
+ "score_mean_target": "score_mean",
78
+ "score_magnitude_weight": "alpha",
79
+ "score_variance_weight": "beta",
80
+ "score_mean_weight": "gamma",
81
+ }
82
+ normalized = dict(config)
83
+ for new_name, old_name in aliases.items():
84
+ if new_name not in normalized and old_name in normalized:
85
+ normalized[new_name] = normalized[old_name]
86
+ super().__init__(normalized)
87
+ if self.config["target_dim"] != 1:
88
+ raise ValueError(
89
+ "ISATPN requires target_dim=1 because positive and negative "
90
+ "branches are subtracted into one prediction."
91
+ )
92
+
93
+
94
+ hidden_dim = self.config["hidden_dim"]
95
+ gcn_layers = self.config["conv_layers"]
96
+ dropout = self.config["dropout"]
97
+ linear_layers = self.config["linear_layers"]
98
+ last_linear_dim = int(hidden_dim/(2**linear_layers))
99
+ if last_linear_dim < 1:
100
+ raise ValueError(
101
+ "ISATPN requires hidden_dim >= 2 ** linear_layers; got "
102
+ f"hidden_dim={hidden_dim}, linear_layers={linear_layers}."
103
+ )
104
+
105
+ self.embedding_rnode_lin = nn.Sequential(
106
+ nn.Linear(self.config["node_dim"], hidden_dim, bias=False)
107
+ )
108
+ self.embedding_inode_lin = nn.Sequential(
109
+ nn.Linear(1, hidden_dim, bias=False)
110
+ )
111
+ self.embedding_edge_lin = nn.Sequential(
112
+ nn.Linear(self.config["edge_dim"], hidden_dim, bias=False)
113
+ )
114
+ self.ISATconv_PM = ISATconvolution_PM(hidden_dim, hidden_dim, hidden_dim, nn.LeakyReLU(), gcn_layers, dropout, False, True, 0.1)
115
+
116
+ self.linears = Linears(hidden_dim,last_linear_dim, nn.ReLU(), linear_layers, dropout, False, False)
117
+ self.out_linear = nn.Linear(last_linear_dim, self.config["target_dim"])
118
+
119
+ self.Bi_Dropout=Bi_Dropout(self.config["bidirectional_dropout"])
120
+
121
+ self.score_var = self.config["score_variance_target"]
122
+ self.score_mean = self.config["score_mean_target"]
123
+ self.alpha = self.config["score_magnitude_weight"]
124
+ self.beta = self.config["score_variance_weight"]
125
+ self.gamma = self.config["score_mean_weight"]
126
+
127
+
128
+ def forward(self, **kargs):
129
+ graph = kargs.get('compound_graphs', kargs.get('graph'))
130
+ r_node = kargs.get('compound_r_node', kargs.get('r_node'))
131
+ i_node = kargs.get('compound_i_node', kargs.get('i_node'))
132
+ r_edge = kargs.get('compound_r2r_edge', kargs.get('r_edge'))
133
+ d_edge = kargs.get('compound_d2d_edge', kargs.get('d_edge'))
134
+ missing = [
135
+ name
136
+ for name, value in {
137
+ "compound_graphs": graph,
138
+ "compound_r_node": r_node,
139
+ "compound_i_node": i_node,
140
+ "compound_r2r_edge": r_edge,
141
+ "compound_d2d_edge": d_edge,
142
+ }.items()
143
+ if value is None
144
+ ]
145
+ if missing:
146
+ raise ValueError(f"ISATPN input is missing required fields {missing!r}.")
147
+
148
+ self.Bi_Dropout.set_drop_p(self.training)
149
+
150
+ r_node = r_node.float()
151
+ r_node = self.embedding_rnode_lin(r_node)
152
+ i_node = i_node.float()
153
+ i_node = self.embedding_inode_lin(i_node)
154
+ r_edge = r_edge.float()
155
+ r_edge = self.embedding_edge_lin(r_edge)
156
+
157
+ real_graph = relation_graph(graph, 'r_nd', 'r2r')
158
+
159
+ r_node_P, r_node_M, score_P, score_M = self.ISATconv_PM(graph, r_node, r_edge, i_node, d_edge, **kargs)
160
+ if kargs.get('get_feature',False):
161
+ return {'feature_P':r_node_P,'feature_N':r_node_M,'positive':score_P,'negative':score_M}
162
+
163
+ h_P = graph_sum_pool(real_graph, r_node_P)
164
+ h_P = self.linears(h_P)
165
+ h_P = self.out_linear(h_P)
166
+
167
+ h_M = graph_sum_pool(real_graph, r_node_M)
168
+ h_M = self.linears(h_M)
169
+ h_M = self.out_linear(h_M)
170
+
171
+ h=torch.concat([h_P,h_M],axis=1)
172
+ h=self.Bi_Dropout(h)
173
+ output=torch.unsqueeze(h[:,0]-h[:,1],axis=1)
174
+ if kargs.get('get_score',False):
175
+ return {'prediction':output, 'positive':score_P,'negative':score_M}
176
+
177
+ self.p_score_var = self.ISATconv_PM.p_score_var
178
+ self.n_score_var = self.ISATconv_PM.n_score_var
179
+ self.p_score_mean = self.ISATconv_PM.p_score_mean
180
+ self.n_score_mean = self.ISATconv_PM.n_score_mean
181
+ self.p_score_ms = self.ISATconv_PM.p_score_ms
182
+ self.n_score_ms = self.ISATconv_PM.n_score_ms
183
+
184
+ return output
185
+
186
+ def compute_loss(self, scores, targets):
187
+ mask = ~torch.isnan(targets)
188
+ if not torch.any(mask):
189
+ raise ValueError("A batch contains no finite target values for loss.")
190
+ scores,targets=self.Bi_Dropout.drop_label(scores[mask],targets[mask])
191
+ return torch.mean(torch.square(scores-targets))+\
192
+ (self.p_score_ms+self.n_score_ms)*self.alpha +\
193
+ (torch.abs(self.score_var-self.p_score_var)+torch.abs(self.score_var-self.n_score_var))*self.beta +\
194
+ (torch.abs(self.score_mean-self.p_score_mean)+torch.abs(self.score_mean-self.n_score_mean))*self.gamma
195
+
196
+
197
+ network = ISATPN
198
+
199
+
@@ -0,0 +1,90 @@
1
+ import torch.nn as nn
2
+
3
+ from D4CMPP2.networks.base import (
4
+ InputContract,
5
+ ISA_HYPERPARAMETERS,
6
+ ISA_OPTIMIZATION_SPACE,
7
+ MolecularNetwork,
8
+ )
9
+ from D4CMPP2.networks.src.ISAT import ISATconvolution
10
+ from D4CMPP2.networks.src.Linear import Linears
11
+ from D4CMPP2.networks.src.GCN import graph_sum_pool
12
+ from D4CMPP2.networks.src.pyg_hetero import relation_graph
13
+
14
+ class ISAT(MolecularNetwork):
15
+ """Interpretable structure-attention network."""
16
+
17
+ model_name = "isat"
18
+ required_config = ("node_dim", "edge_dim", "target_dim")
19
+ input_contract = InputContract(
20
+ required=(
21
+ "compound_graphs",
22
+ "compound_r_node",
23
+ "compound_i_node",
24
+ "compound_r2r_edge",
25
+ "compound_d2d_edge",
26
+ ),
27
+ optional=("get_score",),
28
+ )
29
+ hyperparameters = ISA_HYPERPARAMETERS
30
+ default_optimization_space = ISA_OPTIMIZATION_SPACE
31
+
32
+ def __init__(self, config):
33
+ super().__init__(config)
34
+
35
+ hidden_dim = self.config["hidden_dim"]
36
+ gcn_layers = self.config["conv_layers"]
37
+ dropout = self.config["dropout"]
38
+ linear_layers = self.config["linear_layers"]
39
+ target_dim = self.config["target_dim"]
40
+
41
+ self.embedding_rnode_lin = nn.Sequential(
42
+ nn.Linear(self.config["node_dim"], hidden_dim, bias=False)
43
+ )
44
+ self.embedding_inode_lin = nn.Sequential(
45
+ nn.Linear(1, hidden_dim, bias=False)
46
+ )
47
+ self.embedding_edge_lin = nn.Sequential(
48
+ nn.Linear(self.config["edge_dim"], hidden_dim, bias=False)
49
+ )
50
+ self.ISATconv = ISATconvolution(hidden_dim, hidden_dim, hidden_dim, nn.LeakyReLU(), gcn_layers,dropout, False, True, 0.1)
51
+
52
+ self.linears = Linears(hidden_dim, target_dim, nn.ReLU(), linear_layers, dropout, False, False, last=True)
53
+
54
+ def forward(self, **kargs):
55
+ graph = kargs.get('compound_graphs', kargs.get('graph'))
56
+ r_node = kargs.get('compound_r_node', kargs.get('r_node'))
57
+ i_node = kargs.get('compound_i_node', kargs.get('i_node'))
58
+ r_edge = kargs.get('compound_r2r_edge', kargs.get('r_edge'))
59
+ d_edge = kargs.get('compound_d2d_edge', kargs.get('d_edge'))
60
+ missing = [
61
+ name
62
+ for name, value in {
63
+ "compound_graphs": graph,
64
+ "compound_r_node": r_node,
65
+ "compound_i_node": i_node,
66
+ "compound_r2r_edge": r_edge,
67
+ "compound_d2d_edge": d_edge,
68
+ }.items()
69
+ if value is None
70
+ ]
71
+ if missing:
72
+ raise ValueError(f"ISAT input is missing required fields {missing!r}.")
73
+
74
+ r_node = r_node.float()
75
+ r_node = self.embedding_rnode_lin(r_node)
76
+ i_node = i_node.float()
77
+ i_node = self.embedding_inode_lin(i_node)
78
+ r_edge = r_edge.float()
79
+ r_edge = self.embedding_edge_lin(r_edge)
80
+
81
+ r_node, score = self.ISATconv(graph, r_node, r_edge, i_node, d_edge)
82
+ h = graph_sum_pool(relation_graph(graph, 'r_nd', 'r2r'), r_node)
83
+ h = self.linears(h)
84
+
85
+ if kargs.get('get_score',False):
86
+ return {'prediction':h, 'positive':score}
87
+
88
+ return h
89
+
90
+ network = ISAT
@@ -0,0 +1,56 @@
1
+ import torch.nn as nn
2
+ from D4CMPP2.networks.base import (
3
+ InputContract,
4
+ MolecularNetwork,
5
+ STANDARD_GRAPH_HYPERPARAMETERS,
6
+ STANDARD_GRAPH_OPTIMIZATION_SPACE,
7
+ )
8
+ from D4CMPP2.networks.src.MPNN import MPNNs
9
+ from D4CMPP2.networks.src.GCN import graph_sum_pool
10
+ from D4CMPP2.networks.src.Linear import Linears
11
+
12
+ class MPNN(MolecularNetwork):
13
+ """Edge-aware message-passing network with graph-sum readout."""
14
+
15
+ model_name = "mpnn"
16
+ required_config = ("node_dim", "edge_dim", "target_dim")
17
+ input_contract = InputContract(
18
+ required=(
19
+ "compound_graphs",
20
+ "compound_node_feature",
21
+ "compound_edge_feature",
22
+ )
23
+ )
24
+ hyperparameters = STANDARD_GRAPH_HYPERPARAMETERS
25
+ default_optimization_space = STANDARD_GRAPH_OPTIMIZATION_SPACE
26
+
27
+ def __init__(self, config):
28
+ super().__init__(config)
29
+
30
+ hidden_dim = self.config["hidden_dim"]
31
+ mpnn_layers = self.config["conv_layers"]
32
+ dropout = self.config["dropout"]
33
+ linear_layers = self.config["linear_layers"]
34
+ target_dim = self.config["target_dim"]
35
+
36
+
37
+ self.node_embedding = nn.Linear(self.config["node_dim"], hidden_dim)
38
+ self.edge_embedding = nn.Linear(self.config["edge_dim"], hidden_dim)
39
+
40
+ self.MPNNs = MPNNs(hidden_dim, hidden_dim, hidden_dim, hidden_dim, nn.LeakyReLU(), mpnn_layers, dropout, False, True) # in_feats, hidden_feats, out_feats, activation, n_layers, dropout=0.2, batch_norm=False, residual_sum = False):
41
+ self.Linears = Linears(hidden_dim,target_dim, nn.LeakyReLU(), linear_layers, dropout, False, False, last=True) # in_feats, out_feats, activation, n_layers, dropout=0.2, batch_norm=False, residual_sum = False):
42
+
43
+ def forward(self, **kwargs):
44
+ self.validate_input(kwargs)
45
+ graph = kwargs["compound_graphs"]
46
+ node_feats = kwargs["compound_node_feature"]
47
+ edge_feats = kwargs["compound_edge_feature"]
48
+
49
+ h = self.node_embedding(node_feats)
50
+ e = self.edge_embedding(edge_feats)
51
+ h = self.MPNNs(graph, h, e)
52
+ h = graph_sum_pool(graph, h)
53
+ h = self.Linears(h)
54
+ return h
55
+
56
+ network = MPNN
@@ -0,0 +1,72 @@
1
+ import torch.nn as nn
2
+ from D4CMPP2.networks.base import (
3
+ InputContract,
4
+ MolecularNetwork,
5
+ STANDARD_SOLVENT_HYPERPARAMETERS,
6
+ STANDARD_SOLVENT_OPTIMIZATION_SPACE,
7
+ )
8
+ from D4CMPP2.networks.src.MPNN import MPNNs
9
+ from D4CMPP2.networks.src.GCN import graph_sum_pool
10
+ from D4CMPP2.networks.src.Linear import Linears
11
+ from D4CMPP2.networks.src.SolventLayer import SolventLayer
12
+
13
+ class SolventMPNN(MolecularNetwork):
14
+ """Edge-aware compound MPNN coupled to a solvent graph branch."""
15
+
16
+ model_name = "mpnn_solvent"
17
+ required_config = ("node_dim", "edge_dim", "target_dim")
18
+ input_contract = InputContract(
19
+ required=(
20
+ "compound_graphs",
21
+ "compound_node_feature",
22
+ "compound_edge_feature",
23
+ "solvent_graphs",
24
+ "solvent_node_feature",
25
+ )
26
+ )
27
+ hyperparameters = STANDARD_SOLVENT_HYPERPARAMETERS
28
+ default_optimization_space = STANDARD_SOLVENT_OPTIMIZATION_SPACE
29
+
30
+ def __init__(self, config):
31
+ super().__init__(config)
32
+
33
+ hidden_dim = self.config["hidden_dim"]
34
+ mpnn_layers = self.config["conv_layers"]
35
+ dropout = self.config["dropout"]
36
+ linear_layers = self.config["linear_layers"]
37
+
38
+ self.node_embedding = nn.Linear(self.config["node_dim"], hidden_dim)
39
+ self.edge_embedding = nn.Linear(self.config["edge_dim"], hidden_dim)
40
+ self.MPNNs = MPNNs(hidden_dim, hidden_dim, hidden_dim, hidden_dim, nn.ReLU(), mpnn_layers, dropout, False, True) # in_feats, hidden_feats, out_feats, activation, n_layers, dropout=0.2, batch_norm=False, residual_sum = False):
41
+ self.Linears = Linears(hidden_dim,hidden_dim, nn.ReLU(), linear_layers, dropout, False, False) # in_feats, out_feats, activation, n_layers, dropout=0.2, batch_norm=False, residual_sum = False):
42
+ self.solvlayer = SolventLayer(self.config)
43
+
44
+
45
+ def forward(self, **kwargs):
46
+ graph = kwargs.get('compound_graphs', kwargs.get('graph'))
47
+ node_feats = kwargs.get('compound_node_feature', kwargs.get('node_feats'))
48
+ edge_feats = kwargs.get('compound_edge_feature', kwargs.get('edge_feats'))
49
+ solv_graph = kwargs.get('solvent_graphs', kwargs.get('solv_graph'))
50
+ solv_node_feats = kwargs.get('solvent_node_feature', kwargs.get('solv_node_feats'))
51
+ values = {
52
+ "compound_graphs": graph,
53
+ "compound_node_feature": node_feats,
54
+ "compound_edge_feature": edge_feats,
55
+ "solvent_graphs": solv_graph,
56
+ "solvent_node_feature": solv_node_feats,
57
+ }
58
+ missing = [name for name, value in values.items() if value is None]
59
+ if missing:
60
+ raise ValueError(
61
+ f"SolventMPNN input is missing required fields {missing!r}."
62
+ )
63
+
64
+ h = self.node_embedding(node_feats)
65
+ e = self.edge_embedding(edge_feats)
66
+ h = self.MPNNs(graph, h, e)
67
+ h = graph_sum_pool(graph, h)
68
+ h = self.Linears(h)
69
+ h = self.solvlayer(h, solv_graph, solv_node_feats)
70
+ return h
71
+
72
+ network = SolventMPNN
@@ -0,0 +1,25 @@
1
+ """Public contracts for built-in and custom D4CMPP2 networks."""
2
+
3
+ from D4CMPP2.networks.base import (
4
+ Hyperparameter,
5
+ InputContract,
6
+ ModelConfig,
7
+ MolecularNetwork,
8
+ )
9
+ from D4CMPP2.networks.registry import (
10
+ ModelDefinition,
11
+ get_model,
12
+ register_network,
13
+ registered_models,
14
+ )
15
+
16
+ __all__ = [
17
+ "Hyperparameter",
18
+ "InputContract",
19
+ "ModelConfig",
20
+ "ModelDefinition",
21
+ "MolecularNetwork",
22
+ "get_model",
23
+ "register_network",
24
+ "registered_models",
25
+ ]
@@ -0,0 +1,250 @@
1
+ """Small, explicit contracts shared by D4CMPP2 molecular networks."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from types import MappingProxyType
6
+ from typing import ClassVar, Mapping
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+
11
+
12
+ _MISSING = object()
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Hyperparameter:
17
+ """One validated model field and its optional optimization domain."""
18
+
19
+ kind: str
20
+ default: object = _MISSING
21
+ low: float | int | None = None
22
+ high: float | int | None = None
23
+ search_low: float | int | None = None
24
+ search_high: float | int | None = None
25
+ step: float | int | None = None
26
+ values: tuple[object, ...] = ()
27
+ scale: str = "linear"
28
+ grid: tuple[object, ...] = ()
29
+ description: str = ""
30
+
31
+ def validate(self, name, value):
32
+ if self.kind == "int":
33
+ if isinstance(value, bool) or not isinstance(value, int):
34
+ raise TypeError(f"{name} must be an integer, got {value!r}.")
35
+ elif self.kind == "float":
36
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
37
+ raise TypeError(f"{name} must be a number, got {value!r}.")
38
+ value = float(value)
39
+ elif self.kind == "categorical":
40
+ if value not in self.values:
41
+ raise ValueError(
42
+ f"{name} must be one of {list(self.values)!r}, got {value!r}."
43
+ )
44
+ return value
45
+ else:
46
+ raise ValueError(
47
+ f"Hyperparameter {name!r} has unknown kind {self.kind!r}."
48
+ )
49
+ if self.low is not None and value < self.low:
50
+ raise ValueError(f"{name} must be >= {self.low}, got {value!r}.")
51
+ if self.high is not None and value > self.high:
52
+ raise ValueError(f"{name} must be <= {self.high}, got {value!r}.")
53
+ return value
54
+
55
+
56
+ STANDARD_GRAPH_HYPERPARAMETERS = MappingProxyType(
57
+ {
58
+ "hidden_dim": Hyperparameter(
59
+ "int", default=64, low=1, search_low=16, search_high=256,
60
+ step=16, grid=(32, 64, 128, 256),
61
+ description="Hidden graph representation width.",
62
+ ),
63
+ "conv_layers": Hyperparameter(
64
+ "int", default=4, low=1, search_low=1, search_high=8,
65
+ step=1, grid=(2, 4, 6, 8),
66
+ description="Number of message-passing layers.",
67
+ ),
68
+ "linear_layers": Hyperparameter(
69
+ "int", default=2, low=1, search_low=1, search_high=5,
70
+ step=1, grid=(1, 2, 3, 4),
71
+ description="Number of prediction-head linear layers.",
72
+ ),
73
+ "dropout": Hyperparameter(
74
+ "float", default=0.2, low=0.0, high=0.5,
75
+ search_low=0.0, search_high=0.5,
76
+ grid=(0.0, 0.1, 0.2, 0.3, 0.5),
77
+ description="Dropout probability.",
78
+ ),
79
+ }
80
+ )
81
+ STANDARD_GRAPH_OPTIMIZATION_SPACE = tuple(STANDARD_GRAPH_HYPERPARAMETERS)
82
+ STANDARD_SOLVENT_HYPERPARAMETERS = MappingProxyType(
83
+ {
84
+ **STANDARD_GRAPH_HYPERPARAMETERS,
85
+ "solvent_hidden_dim": Hyperparameter(
86
+ "int", default=64, low=1, search_low=16, search_high=256,
87
+ step=16, grid=(32, 64, 128),
88
+ description="Hidden width of the solvent graph branch.",
89
+ ),
90
+ "solvent_conv_layers": Hyperparameter(
91
+ "int", default=4, low=1, search_low=1, search_high=8,
92
+ step=1, grid=(2, 4, 6),
93
+ description="Number of solvent graph convolution layers.",
94
+ ),
95
+ "solvent_linear_layers": Hyperparameter(
96
+ "int", default=2, low=1, search_low=1, search_high=5,
97
+ step=1, grid=(1, 2, 3),
98
+ description="Number of solvent projection layers.",
99
+ ),
100
+ "solvent_dropout": Hyperparameter(
101
+ "float", default=0.2, low=0.0, high=0.5,
102
+ search_low=0.0, search_high=0.5,
103
+ grid=(0.0, 0.1, 0.2, 0.3, 0.5),
104
+ description="Dropout probability in the solvent branch.",
105
+ ),
106
+ }
107
+ )
108
+ STANDARD_SOLVENT_OPTIMIZATION_SPACE = tuple(STANDARD_SOLVENT_HYPERPARAMETERS)
109
+ STANDARD_SOLVENT_CONFIG_ALIASES = MappingProxyType(
110
+ {
111
+ "solv_hidden_dim": "solvent_hidden_dim",
112
+ "solv_conv_layers": "solvent_conv_layers",
113
+ "solv_linear_layers": "solvent_linear_layers",
114
+ "solv_dropout": "solvent_dropout",
115
+ }
116
+ )
117
+ ISA_HYPERPARAMETERS = MappingProxyType(
118
+ {
119
+ **STANDARD_GRAPH_HYPERPARAMETERS,
120
+ "linear_layers": Hyperparameter(
121
+ "int", default=4, low=1, high=4,
122
+ search_low=1, search_high=4, step=1, grid=(1, 2, 3, 4),
123
+ description="Number of prediction-head linear layers.",
124
+ ),
125
+ "dropout": Hyperparameter(
126
+ "float", default=0.1, low=0.0, high=0.5,
127
+ search_low=0.0, search_high=0.5,
128
+ grid=(0.0, 0.1, 0.2, 0.3, 0.5),
129
+ description="Dropout probability.",
130
+ ),
131
+ }
132
+ )
133
+ ISA_OPTIMIZATION_SPACE = tuple(ISA_HYPERPARAMETERS)
134
+
135
+
136
+ class ModelConfig(Mapping):
137
+ """Immutable, model-only view of the larger training configuration."""
138
+
139
+ def __init__(self, values):
140
+ self._values = MappingProxyType(dict(values))
141
+
142
+ def __getitem__(self, key):
143
+ return self._values[key]
144
+
145
+ def __iter__(self):
146
+ return iter(self._values)
147
+
148
+ def __len__(self):
149
+ return len(self._values)
150
+
151
+ def __repr__(self):
152
+ return f"ModelConfig({dict(self._values)!r})"
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class InputContract:
157
+ required: tuple[str, ...]
158
+ optional: tuple[str, ...] = ()
159
+
160
+ def validate(self, batch):
161
+ missing = [name for name in self.required if name not in batch]
162
+ if missing:
163
+ raise ValueError(
164
+ f"Network input is missing required fields {missing!r}. "
165
+ f"Required fields: {list(self.required)!r}."
166
+ )
167
+
168
+
169
+ def masked_mse(prediction, target):
170
+ mask = ~torch.isnan(target)
171
+ if not torch.any(mask):
172
+ raise ValueError("A batch contains no finite target values for loss.")
173
+ return torch.mean((prediction[mask] - target[mask]) ** 2)
174
+
175
+
176
+ class MolecularNetwork(nn.Module, ABC):
177
+ """Base contract for trainable molecular property networks."""
178
+
179
+ model_name: ClassVar[str]
180
+ input_contract: ClassVar[InputContract]
181
+ required_config: ClassVar[tuple[str, ...]] = (
182
+ "node_dim",
183
+ "target_dim",
184
+ )
185
+ hyperparameters: ClassVar[Mapping[str, Hyperparameter]] = {}
186
+ default_optimization_space: ClassVar[tuple[str, ...]] = ()
187
+ config_aliases: ClassVar[Mapping[str, str]] = STANDARD_SOLVENT_CONFIG_ALIASES
188
+
189
+ def __init__(self, config):
190
+ super().__init__()
191
+ self.config = self.validate_config(config)
192
+
193
+ @classmethod
194
+ def validate_config(cls, config):
195
+ normalized = dict(config)
196
+ for old_name, canonical_name in cls.config_aliases.items():
197
+ if old_name in normalized:
198
+ if (
199
+ canonical_name in normalized
200
+ and normalized[canonical_name] != normalized[old_name]
201
+ ):
202
+ raise ValueError(
203
+ f"{cls.__name__} received conflicting values for legacy "
204
+ f"{old_name!r} and canonical {canonical_name!r}."
205
+ )
206
+ normalized.setdefault(canonical_name, normalized[old_name])
207
+ missing = [name for name in cls.required_config if name not in normalized]
208
+ if missing:
209
+ raise ValueError(
210
+ f"{cls.__name__} config is missing required fields {missing!r}."
211
+ )
212
+ values = {name: normalized[name] for name in cls.required_config}
213
+ for name, field in cls.hyperparameters.items():
214
+ if name in normalized and normalized[name] is not None:
215
+ value = normalized[name]
216
+ elif field.default is not _MISSING:
217
+ value = field.default
218
+ else:
219
+ raise ValueError(
220
+ f"{cls.__name__} config requires hyperparameter {name!r}."
221
+ )
222
+ values[name] = field.validate(name, value)
223
+ return ModelConfig(values)
224
+
225
+ @classmethod
226
+ def optimization_space(cls, keys=None):
227
+ selected = (
228
+ cls.default_optimization_space if keys is None else tuple(keys)
229
+ )
230
+ unknown = [name for name in selected if name not in cls.hyperparameters]
231
+ if unknown:
232
+ raise ValueError(
233
+ f"{cls.model_name} does not define hyperparameters {unknown!r}. "
234
+ f"Available keys: {sorted(cls.hyperparameters)!r}."
235
+ )
236
+ return {name: cls.hyperparameters[name] for name in selected}
237
+
238
+ def validate_input(self, batch):
239
+ self.input_contract.validate(batch)
240
+
241
+ def compute_loss(self, prediction, target):
242
+ return masked_mse(prediction, target)
243
+
244
+ def loss_fn(self, prediction, target):
245
+ """Temporary training-manager bridge during the staged migration."""
246
+ return self.compute_loss(prediction, target)
247
+
248
+ @abstractmethod
249
+ def forward(self, **batch):
250
+ raise NotImplementedError