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.
- D4CMPP2/_Data/AGENTS.md +24 -0
- D4CMPP2/_Data/Aqsoldb.csv +9291 -0
- D4CMPP2/_Data/BradleyMP.csv +3042 -0
- D4CMPP2/_Data/Lipophilicity.csv +1131 -0
- D4CMPP2/_Data/README.md +8 -0
- D4CMPP2/_Data/__init__.py +26 -0
- D4CMPP2/_Data/optical.csv +20237 -0
- D4CMPP2/_Data/test.csv +190 -0
- D4CMPP2/__init__.py +16 -0
- D4CMPP2/__main__.py +5 -0
- D4CMPP2/_main.py +500 -0
- D4CMPP2/cli.py +7 -0
- D4CMPP2/exceptions.py +53 -0
- D4CMPP2/grid_search.py +259 -0
- D4CMPP2/network_refer.yaml +160 -0
- D4CMPP2/networks/AFP_model.py +72 -0
- D4CMPP2/networks/AFPwithSolv_model.py +72 -0
- D4CMPP2/networks/DMPNN_model.py +90 -0
- D4CMPP2/networks/DMPNNwithSolv_model.py +89 -0
- D4CMPP2/networks/GAT_model.py +48 -0
- D4CMPP2/networks/GATwithSolv_model.py +63 -0
- D4CMPP2/networks/GCN_model.py +113 -0
- D4CMPP2/networks/GCNwithSolv_model.py +103 -0
- D4CMPP2/networks/GC_model.py +122 -0
- D4CMPP2/networks/ISATPM_model.py +14 -0
- D4CMPP2/networks/ISATPN_model.py +199 -0
- D4CMPP2/networks/ISAT_model.py +90 -0
- D4CMPP2/networks/MPNN_model.py +56 -0
- D4CMPP2/networks/MPNNwithSolv_model.py +72 -0
- D4CMPP2/networks/__init__.py +25 -0
- D4CMPP2/networks/base.py +250 -0
- D4CMPP2/networks/registry.py +187 -0
- D4CMPP2/networks/src/AFP.py +118 -0
- D4CMPP2/networks/src/BiDropout.py +29 -0
- D4CMPP2/networks/src/DMPNN.py +35 -0
- D4CMPP2/networks/src/GAT.py +69 -0
- D4CMPP2/networks/src/GC.py +85 -0
- D4CMPP2/networks/src/GCN.py +71 -0
- D4CMPP2/networks/src/ISAT.py +153 -0
- D4CMPP2/networks/src/Linear.py +49 -0
- D4CMPP2/networks/src/MPNN.py +56 -0
- D4CMPP2/networks/src/SolventLayer.py +62 -0
- D4CMPP2/networks/src/__init__.py +0 -0
- D4CMPP2/networks/src/distGCN.py +21 -0
- D4CMPP2/networks/src/pyg_hetero.py +24 -0
- D4CMPP2/optimize.py +472 -0
- D4CMPP2/src/Analyzer/ISAAnalyzer.py +458 -0
- D4CMPP2/src/Analyzer/ISAPNAnalyzer.py +366 -0
- D4CMPP2/src/Analyzer/ISAwSAnalyzer.py +117 -0
- D4CMPP2/src/Analyzer/MolAnalyzer.py +319 -0
- D4CMPP2/src/Analyzer/__init__.py +54 -0
- D4CMPP2/src/Analyzer/core.py +480 -0
- D4CMPP2/src/Analyzer/factory.py +166 -0
- D4CMPP2/src/Analyzer/interpretation.py +232 -0
- D4CMPP2/src/Analyzer/results.py +101 -0
- D4CMPP2/src/DataManager/Dataset/GraphDataset.py +314 -0
- D4CMPP2/src/DataManager/Dataset/ISAGraphDataset.py +384 -0
- D4CMPP2/src/DataManager/Dataset/__init__.py +0 -0
- D4CMPP2/src/DataManager/GraphGenerator/ISAGraphGenerator.py +223 -0
- D4CMPP2/src/DataManager/GraphGenerator/MolGraphGenerator.py +73 -0
- D4CMPP2/src/DataManager/GraphGenerator/__init__.py +14 -0
- D4CMPP2/src/DataManager/ISADataManager.py +67 -0
- D4CMPP2/src/DataManager/MolDataManager.py +735 -0
- D4CMPP2/src/DataManager/__init__.py +14 -0
- D4CMPP2/src/DataManager/contracts.py +179 -0
- D4CMPP2/src/NetworkManager/ISANetworkManager.py +12 -0
- D4CMPP2/src/NetworkManager/NetworkManager.py +520 -0
- D4CMPP2/src/NetworkManager/__init__.py +14 -0
- D4CMPP2/src/PostProcessor.py +160 -0
- D4CMPP2/src/TrainManager/ISATrainManager.py +26 -0
- D4CMPP2/src/TrainManager/TrainManager.py +254 -0
- D4CMPP2/src/TrainManager/__init__.py +14 -0
- D4CMPP2/src/TrainManager/callbacks.py +119 -0
- D4CMPP2/src/__init__.py +0 -0
- D4CMPP2/src/utils/PATH.py +246 -0
- D4CMPP2/src/utils/__init__.py +0 -0
- D4CMPP2/src/utils/argparser.py +56 -0
- D4CMPP2/src/utils/checkpointing.py +90 -0
- D4CMPP2/src/utils/config_resolution.py +123 -0
- D4CMPP2/src/utils/config_validation.py +370 -0
- D4CMPP2/src/utils/csv_validation.py +105 -0
- D4CMPP2/src/utils/data_quality.py +181 -0
- D4CMPP2/src/utils/featureizer.py +202 -0
- D4CMPP2/src/utils/functional_group.csv +169 -0
- D4CMPP2/src/utils/graph_cache.py +213 -0
- D4CMPP2/src/utils/leaderboard.py +212 -0
- D4CMPP2/src/utils/metrics.py +31 -0
- D4CMPP2/src/utils/module_loader.py +147 -0
- D4CMPP2/src/utils/output.py +80 -0
- D4CMPP2/src/utils/reproducibility.py +70 -0
- D4CMPP2/src/utils/run_manifest.py +175 -0
- D4CMPP2/src/utils/scaler.py +40 -0
- D4CMPP2/src/utils/sculptor.py +713 -0
- D4CMPP2/src/utils/splitting.py +250 -0
- D4CMPP2/src/utils/supportfile_saver.py +94 -0
- D4CMPP2/src/utils/tools.py +156 -0
- D4CMPP2/src/utils/transfer_learning.py +111 -0
- d4cmpp2-0.4.0.dist-info/METADATA +420 -0
- d4cmpp2-0.4.0.dist-info/RECORD +103 -0
- d4cmpp2-0.4.0.dist-info/WHEEL +5 -0
- d4cmpp2-0.4.0.dist-info/entry_points.txt +2 -0
- d4cmpp2-0.4.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|
D4CMPP2/networks/base.py
ADDED
|
@@ -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
|