ml-jku/tox21_xgboost_classifier
0
1## These MolStandardizer classes are due to Paolo Tosco2## It was taken from the FS-Mol github3## (https://github.com/microsoft/FS-Mol/blob/main/fs_mol/preprocessing/utils/4## standardizer.py)5## They ensure that a sequence of standardization operations are applied6## https://gist.github.com/ptosco/7e6b9ab9cc3e44ba0919060beaed198e7 8import os9 10from rdkit import Chem11from rdkit.Chem.MolStandardize import rdMolStandardize12 13HF_TOKEN = os.environ.get("HF_TOKEN")14TOX_SMARTS_PATH = "data/tox_smarts.json"15 16TASKS = [17 "NR-AR",18 "NR-AR-LBD",19 "NR-AhR",20 "NR-Aromatase",21 "NR-ER",22 "NR-ER-LBD",23 "NR-PPAR-gamma",24 "SR-ARE",25 "SR-ATAD5",26 "SR-HSE",27 "SR-MMP",28 "SR-p53",29]30 31USED_200_DESCR = [32 0,33 1,34 2,35 3,36 4,37 5,38 6,39 7,40 8,41 9,42 10,43 11,44 12,45 13,46 14,47 15,48 16,49 25,50 26,51 27,52 28,53 29,54 30,55 31,56 32,57 33,58 34,59 35,60 36,61 37,62 38,63 39,64 40,65 41,66 42,67 43,68 44,69 45,70 46,71 47,72 48,73 49,74 50,75 51,76 52,77 53,78 54,79 55,80 56,81 57,82 58,83 59,84 60,85 61,86 62,87 63,88 64,89 65,90 66,91 67,92 68,93 69,94 70,95 71,96 72,97 73,98 74,99 75,100 76,101 77,102 78,103 79,104 80,105 81,106 82,107 83,108 84,109 85,110 86,111 87,112 88,113 89,114 90,115 91,116 92,117 93,118 94,119 95,120 96,121 97,122 98,123 99,124 100,125 101,126 102,127 103,128 104,129 105,130 106,131 107,132 108,133 109,134 110,135 111,136 112,137 113,138 114,139 115,140 116,141 117,142 118,143 119,144 120,145 121,146 122,147 123,148 124,149 125,150 126,151 127,152 128,153 129,154 130,155 131,156 132,157 133,158 134,159 135,160 136,161 137,162 138,163 139,164 140,165 141,166 142,167 143,168 144,169 145,170 146,171 147,172 148,173 149,174 150,175 151,176 152,177 153,178 154,179 155,180 156,181 157,182 158,183 159,184 160,185 161,186 162,187 163,188 164,189 165,190 166,191 167,192 168,193 169,194 170,195 171,196 172,197 173,198 174,199 175,200 176,201 177,202 178,203 179,204 180,205 181,206 182,207 183,208 184,209 185,210 186,211 187,212 188,213 189,214 190,215 191,216 192,217 193,218 194,219 195,220 196,221 197,222 198,223 199,224 200,225 201,226 202,227 203,228 204,229 205,230 206,231 207,232]233 234 235class Standardizer:236 """237 Simple wrapper class around rdkit Standardizer.238 """239 240 DEFAULT_CANON_TAUT = False241 DEFAULT_METAL_DISCONNECT = False242 MAX_TAUTOMERS = 100243 MAX_TRANSFORMS = 100244 MAX_RESTARTS = 200245 PREFER_ORGANIC = True246 247 def __init__(248 self,249 metal_disconnect=None,250 canon_taut=None,251 ):252 """253 Constructor.254 All parameters are optional.255 :param metal_disconnect: if True, metallorganic complexes are256 disconnected257 :param canon_taut: if True, molecules are converted to their258 canonical tautomer259 """260 super().__init__()261 if metal_disconnect is None:262 metal_disconnect = self.DEFAULT_METAL_DISCONNECT263 if canon_taut is None:264 canon_taut = self.DEFAULT_CANON_TAUT265 self._canon_taut = canon_taut266 self._metal_disconnect = metal_disconnect267 self._taut_enumerator = None268 self._uncharger = None269 self._lfrag_chooser = None270 self._metal_disconnector = None271 self._normalizer = None272 self._reionizer = None273 self._params = None274 275 @property276 def params(self):277 """Return the MolStandardize CleanupParameters."""278 if self._params is None:279 self._params = rdMolStandardize.CleanupParameters()280 self._params.maxTautomers = self.MAX_TAUTOMERS281 self._params.maxTransforms = self.MAX_TRANSFORMS282 self._params.maxRestarts = self.MAX_RESTARTS283 self._params.preferOrganic = self.PREFER_ORGANIC284 self._params.tautomerRemoveSp3Stereo = False285 return self._params286 287 @property288 def canon_taut(self):289 """Return whether tautomer canonicalization will be done."""290 return self._canon_taut291 292 @property293 def metal_disconnect(self):294 """Return whether metallorganic complexes will be disconnected."""295 return self._metal_disconnect296 297 @property298 def taut_enumerator(self):299 """Return the TautomerEnumerator object."""300 if self._taut_enumerator is None:301 self._taut_enumerator = rdMolStandardize.TautomerEnumerator(self.params)302 return self._taut_enumerator303 304 @property305 def uncharger(self):306 """Return the Uncharger object."""307 if self._uncharger is None:308 self._uncharger = rdMolStandardize.Uncharger()309 return self._uncharger310 311 @property312 def lfrag_chooser(self):313 """Return the LargestFragmentChooser object."""314 if self._lfrag_chooser is None:315 self._lfrag_chooser = rdMolStandardize.LargestFragmentChooser(316 self.params.preferOrganic317 )318 return self._lfrag_chooser319 320 @property321 def metal_disconnector(self):322 """Return the MetalDisconnector object."""323 if self._metal_disconnector is None:324 self._metal_disconnector = rdMolStandardize.MetalDisconnector()325 return self._metal_disconnector326 327 @property328 def normalizer(self):329 """Return the Normalizer object."""330 if self._normalizer is None:331 self._normalizer = rdMolStandardize.Normalizer(332 self.params.normalizationsFile, self.params.maxRestarts333 )334 return self._normalizer335 336 @property337 def reionizer(self):338 """Return the Reionizer object."""339 if self._reionizer is None:340 self._reionizer = rdMolStandardize.Reionizer(self.params.acidbaseFile)341 return self._reionizer342 343 def charge_parent(self, mol_in):344 """Sequentially apply a series of MolStandardize operations:345 * MetalDisconnector346 * Normalizer347 * Reionizer348 * LargestFragmentChooser349 * Uncharger350 The net result is that a desalted, normalized, neutral351 molecule with implicit Hs is returned.352 """353 params = Chem.RemoveHsParameters()354 params.removeAndTrackIsotopes = True355 mol_in = Chem.RemoveHs(mol_in, params, sanitize=False)356 if self._metal_disconnect:357 mol_in = self.metal_disconnector.Disconnect(mol_in)358 normalized = self.normalizer.normalize(mol_in)359 Chem.SanitizeMol(normalized)360 normalized = self.reionizer.reionize(normalized)361 Chem.AssignStereochemistry(normalized)362 normalized = self.lfrag_chooser.choose(normalized)363 normalized = self.uncharger.uncharge(normalized)364 # need this to reassess aromaticity on things like365 # cyclopentadienyl, tropylium, azolium, etc.366 Chem.SanitizeMol(normalized)367 return Chem.RemoveHs(Chem.AddHs(normalized))368 369 def standardize_mol(self, mol_in):370 """371 Standardize a single molecule.372 :param mol_in: a Chem.Mol373 :return: * (standardized Chem.Mol, n_taut) tuple374 if success. n_taut will be negative if375 tautomer enumeration was aborted due376 to reaching a limit377 * (None, error_msg) if failure378 This calls self.charge_parent() and, if self._canon_taut379 is True, runs tautomer canonicalization.380 """381 n_tautomers = 0382 if isinstance(mol_in, Chem.Mol):383 name = None384 try:385 name = mol_in.GetProp("_Name")386 except KeyError:387 pass388 if not name:389 name = "NONAME"390 else:391 error = f"Expected SMILES or Chem.Mol as input, got {str(type(mol_in))}"392 return None, error393 try:394 mol_out = self.charge_parent(mol_in)395 except Exception as e:396 error = f"charge_parent FAILED: {str(e).strip()}"397 return None, error398 if self._canon_taut:399 try:400 res = self.taut_enumerator.Enumerate(mol_out, False)401 except TypeError:402 # we are still on the pre-2021 RDKit API403 res = self.taut_enumerator.Enumerate(mol_out)404 except Exception as e:405 # something else went wrong406 error = f"canon_taut FAILED: {str(e).strip()}"407 return None, error408 n_tautomers = len(res)409 if hasattr(res, "status"):410 completed = (411 res.status == rdMolStandardize.TautomerEnumeratorStatus.Completed412 )413 else:414 # we are still on the pre-2021 RDKit API415 completed = len(res) < 1000416 if not completed:417 n_tautomers = -n_tautomers418 try:419 mol_out = self.taut_enumerator.PickCanonical(res)420 except AttributeError:421 # we are still on the pre-2021 RDKit API422 mol_out = max(423 [(self.taut_enumerator.ScoreTautomer(m), m) for m in res]424 )[1]425 except Exception as e:426 # something else went wrong427 error = f"canon_taut FAILED: {str(e).strip()}"428 return None, error429 mol_out.SetProp("_Name", name)430 return mol_out, n_tautomers431 432 433def create_dir(path, is_file=False):434 """Creates the parent directories if a path to a file is given, else create the given directory"""435 436 to_create = os.path.dirname(path) if is_file else path437 if not os.path.exists(to_create):438 os.makedirs(to_create)439 440 441def normalize_config(config: dict):442 """Normalizes a json config recursively by applying a mapping"""443 mapping = {"none": None, "true": True, "false": False}444 new_config = {}445 for key, val in config.items():446 if isinstance(val, dict):447 new_config[key] = normalize_config(val)448 elif isinstance(val, (int, float, str)) and val in mapping:449 new_config[key] = mapping[val]450 else:451 new_config[key] = val452 return new_config453 