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,202 @@
1
+ import numpy as np
2
+ import os
3
+ import hashlib
4
+
5
+ import rdkit.Chem as Chem
6
+ from rdkit.Chem import AllChem, MACCSkeys
7
+
8
+ class InvalidAtomError(Exception):
9
+ pass
10
+
11
+ def get_graph_features(mol):
12
+ return np.array(MACCSkeys.GenMACCSKeys(mol)).astype(float)
13
+
14
+ def get_atom_features(mol):
15
+ atom_features = []
16
+ for atom in mol.GetAtoms():
17
+ atom_features.append(atom_feature(atom))
18
+ return np.concatenate(atom_features, axis=0)
19
+
20
+ def get_atom_features_and_graph_feature(mol, graph_featurizer=None):
21
+ atom_features = get_atom_features(mol)
22
+
23
+ if graph_featurizer is not None:
24
+ graph_feature = graph_featurizer(mol)
25
+ else:
26
+ graph_feature = get_graph_features(mol)
27
+ graph_feature = np.tile(graph_feature, (atom_features.shape[0],1))
28
+ return atom_features, graph_feature
29
+
30
+ def get_bond_features(mol):
31
+ bond_features = []
32
+ for bond in mol.GetBonds():
33
+ bond_features.append(bond_feature(bond))
34
+ if len(bond_features) == 0:
35
+ return np.zeros((1, 10))
36
+ return np.concatenate(bond_features, axis=0)
37
+
38
+
39
+ def bond_feature(bond):
40
+ features = np.concatenate([bond_type(bond),
41
+ bond_stereo(bond)
42
+ ], axis=0).reshape(1,-1)
43
+
44
+ return features
45
+
46
+ def generate_conformer(mol, save_conf = True, xyz_path = None):
47
+ smiles = Chem.MolToSmiles(mol)
48
+ if mol is None:
49
+ return None
50
+ mol = Chem.AddHs(mol)
51
+ AllChem.EmbedMolecule(mol)
52
+ AllChem.EmbedMultipleConfs(mol, 10)
53
+ li = AllChem.UFFOptimizeMoleculeConfs(mol, maxIters=5000)
54
+ eg_li = [e if s==0 else np.inf for s,e in li ]
55
+ idx = eg_li.index(min(eg_li))
56
+ conf = mol.GetConformer(idx)
57
+ if save_conf:
58
+ save_xyz(smiles, conf, xyz_path)
59
+ return conf
60
+
61
+ def get_conformer(mol, do_optimize=False, xyz_path = None):
62
+ smiles = Chem.MolToSmiles(mol)
63
+ if mol is None:
64
+ return None
65
+
66
+ if xyz_path is not None:
67
+ pos, atoms = load_xyz(smiles, xyz_path)
68
+ if pos is None and do_optimize:
69
+ return generate_conformer(mol, save_conf=True ,xyz_path = xyz_path)
70
+ elif pos is None:
71
+ return None
72
+
73
+ conf = Chem.Conformer()
74
+ for i in range(len(atoms)):
75
+ conf.SetAtomPosition(i, pos[i])
76
+ return conf
77
+
78
+ def save_xyz(smiles, conf, xyz_path):
79
+ mol = Chem.MolFromSmiles(smiles)
80
+ m = hashlib.sha256()
81
+ m.update(smiles.encode('utf-8'))
82
+ name = m.hexdigest()
83
+ result=f'{mol.GetNumAtoms()} #{smiles}\n'
84
+ result+=f'# {Chem.GetFormalCharge(mol)} 1\n'
85
+ for i, atom in enumerate(mol.GetAtoms()):
86
+ positions = conf.GetAtomPosition(i)
87
+ result+=f'{atom.GetSymbol()} {positions.x} {positions.y} {positions.z}\n'
88
+ with open(f'{xyz_path}/{name}.xyz','w') as t:
89
+ t.write(result)
90
+ return name
91
+
92
+ def load_xyz(smiles,xyz_path):
93
+ m = hashlib.sha256()
94
+ m.update(smiles.encode('utf-8'))
95
+ name = m.hexdigest()
96
+ path = os.path.join(xyz_path,name+'.xyz')
97
+ if not os.path.exists(path):
98
+ return None, None
99
+ m = hashlib.sha256()
100
+ m.update(smiles.encode('utf-8'))
101
+ name = m.hexdigest()
102
+ with open(path,'r') as t:
103
+ file= t.read()
104
+ xyzs = file.split("\n")[2:]
105
+ pos = []
106
+ atoms = []
107
+ for xyz in xyzs:
108
+ space = '\t' if '\t' in xyz else ' '
109
+ a = xyz.split(space)
110
+ if len(a)!=4 and len(a)!=5: break
111
+ pos.append(np.array([to_float(a[1]),to_float(a[2]),to_float(a[3])]))
112
+ atoms.append(a[0])
113
+ return np.stack(pos), atoms
114
+
115
+ def to_float(a):
116
+ try:
117
+ return float(a)
118
+ except ValueError as exc:
119
+ if '*^' in a:
120
+ return float(a.replace('*^','e'))
121
+ raise ValueError(
122
+ f"XYZ coordinate {a!r} is not numeric. Check the cached XYZ file."
123
+ ) from exc
124
+
125
+
126
+ def get_bond_features_conformer(mol, conf=None, xyz_path=None):
127
+ if conf is None:
128
+ conf = get_conformer(mol, do_optimize=True, xyz_path=xyz_path)
129
+
130
+ bond_features = []
131
+ for bond in mol.GetBonds():
132
+ bond_features.append(bond_feature_conformer(bond, conf))
133
+ return np.concatenate(bond_features, axis=0)
134
+
135
+ def bond_feature_conformer(bond, conf):
136
+ features = np.concatenate([bond_type(bond),
137
+ bond_stereo(bond),
138
+ bond_length(bond, conf)
139
+ ], axis=0).reshape(1,-1)
140
+ return features
141
+
142
+ def bond_length(bond, conf):
143
+ src_pos = conf.GetAtomPosition(bond.GetBeginAtomIdx())
144
+ dst_pos = conf.GetAtomPosition(bond.GetEndAtomIdx())
145
+ return np.array([np.linalg.norm(src_pos-dst_pos)]).astype(float)
146
+
147
+ def atom_feature(atom):
148
+ features = np.concatenate([atom_symbol_HNums(atom),
149
+ atom_degree(atom),
150
+ atom_Aroma(atom),
151
+ atom_Hybrid(atom),
152
+ atom_ring(atom),
153
+ atom_FC(atom)
154
+ ], axis=0).reshape(1,-1)
155
+
156
+ return features
157
+
158
+ def bond_type(bond):
159
+ return np.array(one_of_k_encoding(str(bond.GetBondType()),['SINGLE', 'DOUBLE', 'TRIPLE', 'AROMATIC'])).astype(int)
160
+
161
+ def bond_stereo(bond):
162
+ return np.array(one_of_k_encoding(str(bond.GetStereo()),['STEREONONE', 'STEREOANY', 'STEREOZ', 'STEREOE'])).astype(int)
163
+
164
+ def atom_symbol_HNums(atom):
165
+
166
+ return np.array(one_of_k_encoding(atom.GetSymbol(),
167
+ ['C', 'N', 'O','S', 'H', 'F', 'Cl', 'Br', 'I', 'Se', 'Te','Si','P','B','Sn','Ge'])+
168
+ one_of_k_encoding(atom.GetTotalNumHs(), [0, 1, 2, 3, 4]))
169
+
170
+
171
+ def atom_degree(atom):
172
+ return np.array(one_of_k_encoding(atom.GetDegree(), [0, 1, 2, 3, 4, 5 ,6])).astype(int)
173
+
174
+
175
+ def atom_Aroma(atom):
176
+ return np.array([atom.GetIsAromatic()]).astype(int)
177
+
178
+
179
+ def atom_Hybrid(atom):
180
+ return np.array(one_of_k_encoding(str(atom.GetHybridization()),['S','SP','SP2','SP3','SP3D','SP3D2'],'S')).astype(int)
181
+
182
+
183
+ def atom_ring(atom):
184
+ return np.array([atom.IsInRing()]).astype(int)
185
+
186
+
187
+ def atom_FC(atom):
188
+ return np.array(one_of_k_encoding(atom.GetFormalCharge(), [-4,-3,-2,-1, 0, 1, 2, 3, 4])).astype(int)
189
+
190
+
191
+ def one_of_k_encoding(x, allowable_set,default=None):
192
+ if x not in allowable_set:
193
+ if default is not None:
194
+ return list(map(lambda s: default == s, allowable_set))
195
+ raise InvalidAtomError("input {0} not in allowable set{1}:".format(x, allowable_set))
196
+ return list(map(lambda s: x == s, allowable_set))
197
+
198
+ def triplet_feature(mol, conf, atom1, atom2, atom3):
199
+ return np.array([rdMolTransforms.GetAngleRad(conf, atom1, atom2, atom3)]).astype(float)
200
+
201
+
202
+
@@ -0,0 +1,169 @@
1
+ Name,Formula,SMARTS,Priority,,
2
+ Sulfur,S,[S;!R],-1,,
3
+ Nitrogen,N,[N;!R],-1,,
4
+ Oxygen,O,[O;!R],-1,,
5
+ Alkane,RH,[C;!R],-1,,
6
+ Methyl,C,[CH3],0,,
7
+ Ethyl,CC,[CH2][CH3],0,,
8
+ Prophyl,CCC,[CH2][CH2][CH3],0,,
9
+ Alkene,R2C=CR2,[C;!R]=[C;!R],1,,
10
+ Alkyne,RC≡CR',[C;!R]#[C;!R],1,,
11
+ Allene,R2C=C=CR2,[C;!R]=[C;!R]=[C;!R],1,,
12
+ Cumulene,R2C=C=C=CR2,[C;!R]=[C;!R]=[C;!R]=[C;!R],1,,
13
+ Fluoro,RF,F,1,,
14
+ Chloro,RCl,[Cl],1,,
15
+ Bromo,RBr,[Br],1,,
16
+ Iodo,RI,I,1,,
17
+ Ether,ROR',[OH0;!R],1,,
18
+ Alcohol,ROH,[OH1],1,,
19
+ Aldehyde,RCHO,[CH1;!R]=O,1,,
20
+ Ketone-O,RCOR',[OX1;+0],1,,
21
+ Ketone,RCOR',[CX3;!R]=O,1,,
22
+ Amine,RNH2,[NH2],1,,
23
+ Aldimine / Imine,RCHNH2,[CH1]=[NH1],1,,
24
+ Nitrile / Cyanide,,C#N,1,,
25
+ Isocyanide / Isonitrile,,[N+]#[C-],1,,
26
+ Azide,,N=[N+]=[N-],1,,
27
+ Azo compound / Diimide / Diazene,,[NH0]=[NH0],1,,
28
+ Diazonium salt,,[N+]#N,1,,
29
+ Hydrazine,,[NX3][NX3],1,,
30
+ Enamine,,C=CN,1,,`
31
+ Hydroxylamine,,N[OH1],1,,
32
+ Amine oxide / N-oxide,,[N+;D4][O-],1,,
33
+ Cyanate,,OC#N,1,,
34
+ Isocyanate,,N=C=O,1,,
35
+ Nitro compound,,[N+](=O)[O-],1,,
36
+ Nitroso,,N=O,1,,
37
+ Nitrite,,ON=O,1,,
38
+ Nitrate,,O[N+](=O)[O-],1,,
39
+ Thioether,RSR',[SD2],1,,
40
+ Thiol,RSH,[SH1],1,,
41
+ Thial / Thioaldehyde,RCHS,[CH1](=S),1,,
42
+ Thione / Thioketone,RCSR',[CD3](=S),1,,
43
+ phosphate,,P(=O)(O)(O)(O),1,,
44
+ Carboxylic Acid,RCO2H,[CH0;!R](=O)[OH1],2,,
45
+ Acyl halide,RCOX,"[CH0;!R](=O)[F,Cl,I,Br]",2,,
46
+ Ester,RCO2R',[CH0;!R](=O)[OH0],2,,
47
+ Hydroperoxide,ROOH,[OH0][OH1],2,,
48
+ Peroxide,ROOR',[OH0;!R][OH0;!R],2,,
49
+ Ketene,RC(=C=O)R',[CX3]=C=O,2,,
50
+ Ketimine / Imine,,[CX3]=[NH0],2,,
51
+ Ketenimine,,[CX3]=C=N,2,,
52
+ Carbodiimide,,[NH0]=C=[NH0],2,,
53
+ Amidine,,[CH0;!R](=[NH1])[NH2],2,,
54
+ Amidrazone,,[CH0;!R](=[NH1])[NH1][NH2],2,,
55
+ Guanidine,,N[CH0;!R](=[NH1])[NH1][NH2],2,,
56
+ Hydrazone,,[CX3;!R]=N[NH2],2,,
57
+ Aminal,,[CX4;!R](N)(N),2,,
58
+ Hemiaminal,,[CX4;!R](N)O,2,,
59
+ Aldoxime,,[CH1]=N[OH1],2,,
60
+ Ketoxime,,[CH0;D3]=N[OH1],2,,
61
+ Aldoxime ether,,[CH1]=N[OH0],2,,
62
+ Ketoxime ether,,[CH0;X3]=N[OH0],2,,
63
+ Amide,,[CH0;!R](=O)N,2,,
64
+ Carbamic acid,,NC(=O)[OH1],2,,
65
+ Carbamate,,NC(=O)[OH0],2,,
66
+ Urea,,NC(=O)N,2,,
67
+ Isourea,,NC(=[NH1])O,2,,
68
+ Carbamic acyl halide,,"NC(=O)[F,Cl,I,Br]",2,,
69
+ Acyl cyanide,,[CH0](=O)C#N,2,,
70
+ [[Thioic O-acid]],RC(S)OH,[CH0](=S)[OH1],2,,
71
+ [[Thioic S-acid]],RC(O)SH,[CH0](=O)[SH1],2,,
72
+ Dithioic acid,RCS2H,[CH0](=S)[SH1],2,,
73
+ Thioyl_halide,RCSX,"[CH0](=S)[F,Cl,Br,I]",2,,
74
+ [[Thioic O-ester]],RC(S)OR',[CH0](=S)[OH0],2,,
75
+ [[Thioic S-ester]],RC(O)SR',[CH0](=O)[SH0],2,,
76
+ Dithioic ester,RCS2R',[CH0](=S)[SH0],2,,
77
+ Sulfuric acid,SO3,[SH0](=O)(=O)O,3,,
78
+ Peroxy acid,RCO3H,[CH0;!R](=O)O[OH1],3,,
79
+ Anhydride,RCOOCOR',[CH0;!R](=O)O[CH0](=O),3,,
80
+ Carbonate,ROCOOR',[OH0;!R]C(=O)[OH0],3,,
81
+ Hydrate,RC(OH)(OH)R',[CD4]([OH1])[OH1],3,,
82
+ Hemiacetal,RCH(OR')(OH),[CH1]([OH0])[OH1],3,,
83
+ Acetal,RCH(OR')(OR''),[CH1]([OH0])[OH0],3,,
84
+ Hemiketal,RC(OR'')(OH)R',[CX4]([OH0])[OH1],3,,
85
+ Ketal,RC(OR'')(OR''')R',[CX4]([OH0])[OH0],3,,
86
+ Orthoester,RC(OR')(OR'')(OR'''),[CX4]([OH0])([OH0])[OH0],3,,
87
+ Orthocarbonate,ROC(OR')(OR'')(OR'''),O[CX4](O)(O)O,3,,
88
+ Enol,RC(OH)=CR'2,[CH0;!R]([OH1])=[CD3],3,,
89
+ Enol ether,RC(OR')=CR''2,[CH0;!R]([OH0])=[CD3],3,,
90
+ Hydroxamic acid,,[CH0;!R](=O)[NH0]([OH1]),3,,
91
+ Imide,,[CH0;!R](=O)[NH0][CH0](=O),3,,
92
+ Imidoester,,[CH0;!R](=N)N,3,,
93
+ Imidoyl halide,,"[CH0;!R](=N)[F,Cl,Br,I]",3,,
94
+ Hydrazide,,[CH0;!R](=O)NN,3,,
95
+ Semicarbazide,,[NX3]C(=O)N[NX3],3,,
96
+ Semicarbazone,,[NX3]C(=O)N[NX2],3,,
97
+ α-lactone,,C1(=O)OC1,5,,
98
+ β-lactone,,C1(=O)OCC1,5,,
99
+ γ-lactone,,C1(=O)OCCC1,5,,
100
+ δ-lactone,/,C1(=O)OCCCC1,5,,
101
+ ε-lactone,,C1(=O)OCCCCC1,5,,
102
+ α-Lactam,,C1(=O)[NH1]C1,5,,
103
+ β-Lactam,,C1(=O)[NH1]CC1,5,,
104
+ γ-Lactam,,C1(=O)[NH1]CCC1,5,,
105
+ δ-Lactam,,C1(=O)[NH1]CCCC1,5,,
106
+ ε-Lactam,,C1(=O)[NH1]CCCCC1,5,,
107
+ Epoxide,/,O1CC1,5,,
108
+ ethylenamine,,C1CN1,5,,
109
+ trymethylene oxide,,C1COC1,5,,
110
+ tetrahydrofuran,,C1CCOC1,5,,
111
+ pyrrolidine,,C1CCNC1,5,,
112
+ pyran,,C1=CCOC=C1,5,,
113
+ piperidine,,C1CCNCC1,5,,
114
+ dioxane,,C1COCCO1,5,,
115
+ morpholine,,C1COCCN1,5,,
116
+ Unknown tetra ring,,[R]1@[R]@[R]@[R]@1,5,,
117
+ Unknown penta ring,,[R]1@[R]@[R]@[R]@[R]@1,5,,
118
+ Unknown hexa ring,,[R]1@[R]@[R]@[R]@[R]@[R]@1,5,,
119
+ Unknown septa ring,,[R]1@[R]@[R]@[R]@[R]@[R]@[R]@1,5,,
120
+ Unknown octa ring,,[R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1,5,,
121
+ Unknown nona ring,,[R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1,5,,
122
+ Unknown deca ring,,[R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1,5,,
123
+ Unknown tetra aromatic ring,,[a]1[a][a][a]1,5,,
124
+ Unknown penta aromatic ring,,[a]1[a][a][a][a]1,5,,
125
+ Unknown hexa aromatic ring,,[a]1[a][a][a][a][a]1,5,,
126
+ Unknown septa aromatic ring,,[a]1[a][a][a][a][a][a]1,5,,
127
+ Unknown octa aromatic ring,,[a]1[a][a][a][a][a][a][a]1,5,,
128
+ Unknown nona aromatic ring,,[a]1[a][a][a][a][a][a][a][a]1,5,,
129
+ Unknown deca aromatic ring,,[a]1[a][a][a][a][a][a][a][a][a]1,5,,
130
+ furan,,c1ccoc1,6,,
131
+ thiophene,,c1ccsc1,6,,
132
+ pyrrole,,c1ccnc1,6,,
133
+ Benzene,,c1ccccc1,6,,
134
+ pyridine,,c1ccncc1,6,,
135
+ imidazole,,c1cncn1,6,,
136
+ thiazole,,c1cscn1,6,,
137
+ pyrimidine,,c1cncnc1,6,,
138
+ naphthalene,,c1ccc2ccccc2c1,9,,
139
+ anthracene,,c1ccc2cc3ccccc3cc2c1,9,,
140
+ phenanthrene,,c1ccc2c(c1)ccc1ccccc12,9,,
141
+ chrysene,,c1ccc2c(c1)ccc1c3ccccc3ccc21,9,,
142
+ pyrene,,c1cc2ccc3cccc4ccc(c1)c2c34,9,,
143
+ corannulene,,c1cc2ccc3ccc4ccc5ccc1c1c2c3c4c51,9,,
144
+ coronene,,c1cc2ccc3ccc4ccc5ccc6ccc1c1c2c3c4c5c61,9,,
145
+ pentalene,,C1=CC2=CC=CC2=C1,9,,
146
+ indene,,C1=Cc2ccccc2C1,9,,
147
+ azulene,,c1ccc2cccc-2cc1,9,,
148
+ heptalene,,C1=CC=C2C=CC=CC=C2C=C1,9,,
149
+ biphenylene,,c1ccc2c(c1)-c1ccccc1-2,9,,
150
+ as-indacene,,C1=Cc2c3c(ccc2=C1)=CC=C3,9,,
151
+ s-indacene,,C1=Cc2cc3c(cc2=C1)C=CC=3,9,,
152
+ acenaphthylene,,C1=Cc2cccc3cccc1c23,9,,
153
+ fluorene,,c1ccc2c(c1)Cc1ccccc1-2,9,,
154
+ phenalene,,C1=Cc2cccc3cccc(c23)C1,9,,
155
+ perylene,,c1cc2cccc3c4cccc5cccc(c(c1)c23)c54,9,,
156
+ indole,,c1ccc2nccc2c1,9,,
157
+ isoindole,,c1ccc2cncc2c1,9,,
158
+ indolizine,,c1ccn2cccc2c1,9,,
159
+ quinoline,,c1ccc2ncccc2c1,9,,
160
+ isoquinoline,,c1ccc2cnccc2c1,9,,
161
+ purine,,c1ncc2ncnc2n1,9,,
162
+ carbazole,,c1ccc2c(c1)nc1ccccc12,9,,
163
+ dibenzofuran,,c1ccc2c(c1)oc1ccccc12,9,,
164
+ 2H-chromoene,,C1=Cc2ccccc2OC1,9,,
165
+ xanthene,,c1ccc2c(c1)Cc1ccccc1O2,9,,
166
+ porphyrin,,C1=Cc2cc3ccc(cc4nc(cc5ccc(cc1n2)[nH]5)C=C4)[nH]3,9,,
167
+ chlorin,,C1=Cc2cc3ccc(cc4nc(cc5ccc(cc1n2)[nH]5)CC4)[nH]3,9,,
168
+ BODIPY,,F[B-]1(F)N2C=CC=C2C=C2C=CC=[N+]12,9,,
169
+ phthalocyanine,,c1ccc2c(c1)-c1nc-2nc2[nH]c(nc3nc(nc4[nH]c(n1)c1ccccc41)-c1ccccc1-3)c1ccccc21,9,,
@@ -0,0 +1,213 @@
1
+ """Versioned graph-cache identity, validation, and atomic persistence."""
2
+
3
+ import hashlib
4
+ import importlib.metadata
5
+ import json
6
+ import os
7
+ import re
8
+ import uuid
9
+ from pathlib import Path
10
+
11
+ import torch
12
+ from torch_geometric.data import Data, HeteroData
13
+
14
+
15
+ GRAPH_CACHE_SCHEMA_VERSION = 2
16
+ GRAPH_FEATURE_CONTRACT_VERSION = 1
17
+ ISA_RELATIONS = (
18
+ ("r_nd", "r2r", "r_nd"),
19
+ ("r_nd", "r2i", "i_nd"),
20
+ ("i_nd", "i2i", "i_nd"),
21
+ ("i_nd", "i2d", "d_nd"),
22
+ ("d_nd", "d2d", "d_nd"),
23
+ ("d_nd", "d2r", "r_nd"),
24
+ )
25
+
26
+
27
+ def _package_version(name):
28
+ try:
29
+ return importlib.metadata.version(name)
30
+ except importlib.metadata.PackageNotFoundError:
31
+ return None
32
+
33
+
34
+ def _file_sha256(path):
35
+ if not path or not Path(path).is_file():
36
+ return None
37
+ digest = hashlib.sha256()
38
+ with open(path, "rb") as stream:
39
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
40
+ digest.update(chunk)
41
+ return digest.hexdigest()
42
+
43
+
44
+ def _ordered_smiles_sha256(smiles):
45
+ encoded = json.dumps([str(value) for value in smiles], ensure_ascii=False, separators=(",", ":"))
46
+ return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
47
+
48
+
49
+ def safe_cache_component(value):
50
+ name = Path(os.fspath(value).replace("\\", "/")).name
51
+ name = re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("._")
52
+ return name or "data"
53
+
54
+
55
+ def build_graph_recipe(manager, column):
56
+ """Return deterministic identity metadata for one molecule-column cache."""
57
+ is_isa = str(manager.graph_type).startswith("img")
58
+ functional_group = None
59
+ if is_isa:
60
+ candidate = Path(manager.config.get("MODEL_PATH", "")) / "functional_group.csv"
61
+ functional_group = candidate if candidate.is_file() else manager.config.get("FRAG_REF")
62
+ recipe = {
63
+ "graph_backend": "pyg",
64
+ "graph_schema_version": GRAPH_CACHE_SCHEMA_VERSION,
65
+ "feature_contract_version": GRAPH_FEATURE_CONTRACT_VERSION,
66
+ "graph_type": manager.graph_type,
67
+ "molecule_column": column,
68
+ "explicit_h": column in manager.explicit_h_columns,
69
+ "row_count": len(manager._molecule_smiles[column]),
70
+ "ordered_smiles_sha256": _ordered_smiles_sha256(manager._molecule_smiles[column]),
71
+ "node_dim": manager.gg.node_dim,
72
+ "edge_dim": manager.gg.edge_dim,
73
+ "max_dist": manager.config.get("max_dist", 4) if is_isa else None,
74
+ "functional_group_sha256": _file_sha256(functional_group) if is_isa else None,
75
+ "versions": {
76
+ "d4cmpp2": _package_version("D4CMPP2"),
77
+ "rdkit": _package_version("rdkit"),
78
+ "torch": torch.__version__,
79
+ "torch_geometric": _package_version("torch-geometric"),
80
+ },
81
+ }
82
+ identity = dict(recipe)
83
+ identity.pop("versions")
84
+ serialized = json.dumps(identity, sort_keys=True, separators=(",", ":"))
85
+ recipe["fingerprint"] = hashlib.sha256(serialized.encode("utf-8")).hexdigest()
86
+ return recipe
87
+
88
+
89
+ def cache_path(manager, column, recipe):
90
+ parts = [
91
+ safe_cache_component(manager.data),
92
+ safe_cache_component(column),
93
+ safe_cache_component(manager.graph_type),
94
+ ]
95
+ if column in manager.explicit_h_columns:
96
+ parts.append("explicitH")
97
+ parts.extend(["pyg", "v2", recipe["fingerprint"][:16]])
98
+ return Path(manager.config["GRAPH_DIR"]) / ("_".join(parts) + ".pt")
99
+
100
+
101
+ def legacy_cache_paths(manager, column):
102
+ base = f"{manager.data}_{column}_{manager.graph_type}"
103
+ if column in manager.explicit_h_columns:
104
+ base += "_explicitH"
105
+ root = Path(manager.config["GRAPH_DIR"])
106
+ return root / f"{base}_pyg_v1.pt", root / f"{base}.bin"
107
+
108
+
109
+ def atomic_save_graph_cache(payload, path):
110
+ path = Path(path)
111
+ path.parent.mkdir(parents=True, exist_ok=True)
112
+ staging = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
113
+ try:
114
+ torch.save(payload, staging)
115
+ os.replace(staging, path)
116
+ finally:
117
+ if staging.exists():
118
+ staging.unlink()
119
+
120
+
121
+ def _validate_tensor(tensor, name, expected_rows=None, expected_columns=None, floating=None):
122
+ if not isinstance(tensor, torch.Tensor):
123
+ raise ValueError(f"{name} must be a torch.Tensor, got {type(tensor).__name__}.")
124
+ if expected_rows is not None and tensor.shape[0] != expected_rows:
125
+ raise ValueError(f"{name} rows={tensor.shape[0]}, expected {expected_rows}.")
126
+ if expected_columns is not None and (tensor.ndim != 2 or tensor.shape[1] != expected_columns):
127
+ raise ValueError(f"{name} shape={tuple(tensor.shape)}, expected (*, {expected_columns}).")
128
+ if floating is True and not tensor.is_floating_point():
129
+ raise ValueError(f"{name} dtype={tensor.dtype}, expected a floating dtype.")
130
+ if tensor.is_floating_point() and tensor.numel() and not bool(torch.isfinite(tensor).all()):
131
+ raise ValueError(f"{name} contains NaN or infinite values.")
132
+
133
+
134
+ def _validate_edge_alignment(edge_index, edge_attr, name):
135
+ if edge_attr.shape[0] != edge_index.shape[1]:
136
+ raise ValueError(
137
+ f"{name} edge feature rows={edge_attr.shape[0]}, "
138
+ f"but edge_index contains {edge_index.shape[1]} edges."
139
+ )
140
+
141
+
142
+ def validate_graph(graph, recipe, index):
143
+ prefix = f"graphs[{index}]"
144
+ if str(recipe["graph_type"]).startswith("img"):
145
+ if not isinstance(graph, HeteroData):
146
+ raise ValueError(f"{prefix} must be HeteroData, got {type(graph).__name__}.")
147
+ missing_nodes = [node for node in ("r_nd", "i_nd", "d_nd") if node not in graph.node_types]
148
+ missing_relations = [relation for relation in ISA_RELATIONS if relation not in graph.edge_types]
149
+ if missing_nodes or missing_relations:
150
+ raise ValueError(
151
+ f"{prefix} is missing node types {missing_nodes} or relations {missing_relations}."
152
+ )
153
+ _validate_tensor(graph["r_nd"].x, f"{prefix}.r_nd.x", expected_columns=recipe["node_dim"], floating=True)
154
+ for relation in ISA_RELATIONS:
155
+ _validate_tensor(graph[relation].edge_index, f"{prefix}.{relation}.edge_index", expected_rows=2)
156
+ r2r = graph["r_nd", "r2r", "r_nd"]
157
+ _validate_tensor(
158
+ r2r.edge_attr,
159
+ f"{prefix}.r2r.edge_attr",
160
+ expected_columns=recipe["edge_dim"],
161
+ floating=True,
162
+ )
163
+ _validate_edge_alignment(r2r.edge_index, r2r.edge_attr, f"{prefix}.r2r")
164
+ d2d = graph["d_nd", "d2d", "d_nd"]
165
+ _validate_tensor(
166
+ d2d.edge_attr,
167
+ f"{prefix}.d2d.edge_attr",
168
+ expected_columns=recipe["max_dist"],
169
+ floating=True,
170
+ )
171
+ _validate_edge_alignment(d2d.edge_index, d2d.edge_attr, f"{prefix}.d2d")
172
+ return
173
+ if not isinstance(graph, Data) or isinstance(graph, HeteroData):
174
+ raise ValueError(f"{prefix} must be Data, got {type(graph).__name__}.")
175
+ _validate_tensor(graph.x, f"{prefix}.x", expected_columns=recipe["node_dim"], floating=True)
176
+ _validate_tensor(graph.edge_index, f"{prefix}.edge_index", expected_rows=2)
177
+ if graph.edge_index.dtype != torch.long:
178
+ raise ValueError(f"{prefix}.edge_index dtype={graph.edge_index.dtype}, expected torch.int64.")
179
+ _validate_tensor(graph.edge_attr, f"{prefix}.edge_attr", expected_columns=recipe["edge_dim"], floating=True)
180
+ _validate_edge_alignment(graph.edge_index, graph.edge_attr, prefix)
181
+
182
+
183
+ def validate_payload(payload, expected_recipe, expected_smiles, path):
184
+ if not isinstance(payload, dict):
185
+ raise ValueError(f"Graph cache {str(path)!r} must contain a dictionary payload.")
186
+ actual_recipe = payload.get("recipe")
187
+ if not isinstance(actual_recipe, dict):
188
+ raise ValueError(f"Graph cache {str(path)!r} has no v2 recipe metadata.")
189
+ for field, expected in expected_recipe.items():
190
+ if field == "versions":
191
+ continue
192
+ actual = actual_recipe.get(field)
193
+ if actual != expected:
194
+ raise ValueError(
195
+ f"Graph cache {str(path)!r} recipe mismatch for {field!r}: "
196
+ f"actual={actual!r}, expected={expected!r}. Regenerate this cache from the source CSV."
197
+ )
198
+ smiles = payload.get("smiles")
199
+ if [str(value) for value in smiles or []] != [str(value) for value in expected_smiles]:
200
+ raise ValueError(
201
+ f"Graph cache {str(path)!r} ordered SMILES do not match the source CSV. "
202
+ "Regenerate this cache from the current data."
203
+ )
204
+ graphs = payload.get("graphs")
205
+ if not isinstance(graphs, list) or len(graphs) != len(expected_smiles):
206
+ raise ValueError(
207
+ f"Graph cache {str(path)!r} graph count is "
208
+ f"{len(graphs) if isinstance(graphs, list) else type(graphs).__name__}, "
209
+ f"expected {len(expected_smiles)}. Regenerate this cache."
210
+ )
211
+ for index, graph in enumerate(graphs):
212
+ validate_graph(graph, expected_recipe, index)
213
+ return graphs, list(payload.get("graph_errors") or [])