| from typing import Iterable, NamedTuple, Sequence, List |
| import numpy as np |
| import torch |
| from torch import Tensor |
| import logging |
| from torch.utils.data import DataLoader |
| from dataclasses import field, InitVar, dataclass |
| from typing import Iterable, NamedTuple, Sequence |
|
|
|
|
| from chemprop.data.datasets import ( |
| MolAtomBondDatum, |
| MoleculeDataset, |
| MolAtomBondDataset, |
| ReactionDataset, |
| MulticomponentDataset |
| ) |
| from chemprop.data.molgraph import MolGraph |
| from chemprop.data.collate import BatchMolAtomBondGraph |
| from chemprop import data as chemprop_data |
| from chemprop import featurizers |
|
|
| from rdkit import Chem |
| from rdkit.Chem import Descriptors |
|
|
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
|
|
| |
|
|
| import numpy as np |
| from rdkit import Chem |
| from rdkit.Chem import Descriptors |
|
|
| def compute_rdkit_descriptors(smiles_list, output_file=None, save=False): |
| """ |
| Computes a 2D matrix of 217 RDKit physicochemical descriptors from a list of SMILES. |
| """ |
| descriptor_list = Descriptors.descList |
| num_descriptors = len(descriptor_list) |
|
|
| def rdkit_descriptors_from_smiles(smiles_str): |
| """Generates a flat, index-matched list of numerical float values for a single SMILES string""" |
| if not isinstance(smiles_str, str) or not smiles_str: |
| return [0.0] * num_descriptors |
| |
| mol = Chem.MolFromSmiles(smiles_str) |
| if mol is None: |
| return [0.0] * num_descriptors |
|
|
| values = [] |
| for name, func in descriptor_list: |
| try: |
| val = func(mol) |
| |
| if val is None or np.isnan(val) or np.isinf(val): |
| values.append(0.0) |
| else: |
| values.append(float(val)) |
| except Exception: |
| |
| values.append(0.0) |
| return values |
| |
| |
| feature_matrix = [] |
| print(f"Computing descriptors for {len(smiles_list)} molecules...") |
|
|
| |
| for count, s in enumerate(smiles_list): |
| if count % 100 == 0: |
| print(f'Working on molecule {count}') |
| |
| properties_list = rdkit_descriptors_from_smiles(s) |
| feature_matrix.append(properties_list) |
|
|
| |
| feature_matrix = np.array(feature_matrix, dtype=np.float32) |
|
|
| |
| if np.isnan(feature_matrix).any(): |
| feature_matrix = np.nan_to_num(feature_matrix, nan=0.0, posinf=0.0, neginf=0.0) |
| print('NaNs/Infs found in RDKit matrix, zeroed out.') |
|
|
| |
| feature_matrix = np.sign(feature_matrix) * np.log1p(np.abs(feature_matrix)) |
|
|
| |
| feature_matrix = np.clip(feature_matrix, -10.0, 10.0) |
|
|
| |
| if save and output_file: |
| np.savez(output_file, arr=feature_matrix) |
| print(f"Saved descriptors to {output_file}") |
|
|
| return feature_matrix |
|
|
|
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def create_MoleculeDatapoint(smis, ys_flat=None): |
| """ create molecule datapoint objects from smiles """ |
|
|
| if ys_flat is not None: |
| ys = ys_flat.reshape((len(ys_flat), 1)) |
| |
| all_data = [chemprop_data.MoleculeDatapoint.from_smi(smi, y) for smi, y in zip(smis, ys)] |
| else: |
| all_data = [chemprop_data.MoleculeDatapoint.from_smi(smi) for smi in smis] |
| return all_data |
|
|
|
|
| |
| class Datum(NamedTuple): |
| """a singular training data point |
| |
| https://github.com/chemprop/chemprop/blob/420b18834b81aac48963cccdef04e500f1dd0160/chemprop/data/datasets.py#L22 |
| """ |
|
|
| mg: MolGraph |
| V_d: np.ndarray | None |
| x_d: np.ndarray | None |
| y: np.ndarray | None |
| weight: float |
| lt_mask: np.ndarray | None |
| gt_mask: np.ndarray | None |
| smiles: np.ndarray | None |
| rdkit: np.ndarray | None |
| teacher_embs: list[np.ndarray] | None |
|
|
|
|
| class SmilesMoleculeDataset: |
| """ |
| Subclass for MoleculeDataset to track SMILES across batches |
| |
| # Overriding __getitem__ https://github.com/chemprop/chemprop/blob/420b18834b81aac48963cccdef04e500f1dd0160/chemprop/data/datasets.py#L192 |
| |
| """ |
| def __init__(self, smiles, teacher_embeddings=None, labels=None, rdkit=None, |
| featurizer= featurizers.SimpleMoleculeMolGraphFeaturizer(), |
| is_train=False): |
| |
| |
| preprocessed_smiles = create_MoleculeDatapoint(smiles, labels) |
| self._original_dataset = chemprop_data.MoleculeDataset(preprocessed_smiles, featurizer) |
| |
| |
| print("Pre-loading SMILES into memory... (this might take a moment)") |
| |
| self.cached_smiles = list(self._original_dataset.smiles) |
| print(f"Loaded {len(self.cached_smiles)} SMILES strings.") |
| |
|
|
| |
| self._rdkit = rdkit |
| if rdkit is None: |
| self._rdkit = compute_rdkit_descriptors(smiles) |
| |
| self._teacher_embeddings = teacher_embeddings |
|
|
| self.is_train = is_train |
|
|
| def __getitem__(self, idx: int) -> Datum: |
| """ |
| SPED UP version |
| Docstring for __getitem__ |
| |
| :param self: Description |
| :param idx: Description |
| :type idx: int |
| :return: Description |
| :rtype: Datum |
| """ |
|
|
|
|
| d = self._original_dataset.data[idx] |
| mg = self._original_dataset.mg_cache[idx] |
|
|
| smiles = self.cached_smiles[idx] |
| |
| |
| if self.is_train: |
| smiles = randomize_smiles(smiles) |
| |
| |
| |
| rdkit = None if self._rdkit is None else self._rdkit[idx] |
|
|
|
|
| |
| |
| teacher_embs = None |
| if self._teacher_embeddings is not None: |
| |
| teacher_embs = [t_emb[idx] for t_emb in self._teacher_embeddings] |
|
|
| return Datum(mg, self._original_dataset.V_ds[idx], self._original_dataset.X_d[idx], |
| self._original_dataset.Y[idx], d.weight, d.lt_mask, d.gt_mask, smiles, rdkit, teacher_embs) |
|
|
| |
| def __len__(self): |
| return len(self._original_dataset) |
| |
|
|
|
|
|
|
|
|
|
|
| |
|
|
|
|
|
|
|
|
|
|
| |
|
|
|
|
|
|
| @dataclass(repr=False, eq=False, slots=True) |
| class BatchMolGraph: |
| """A :class:`BatchMolGraph` represents a batch of individual :class:`MolGraph`\s. |
| |
| It has all the attributes of a ``MolGraph`` with the addition of the ``batch`` attribute. This |
| class is intended for use with data loading, so it uses :obj:`~torch.Tensor`\s to store data |
| """ |
|
|
| mgs: InitVar[Sequence[MolGraph]] |
| """A list of individual :class:`MolGraph`\s to be batched together""" |
| V: Tensor = field(init=False) |
| """the atom feature matrix""" |
| E: Tensor = field(init=False) |
| """the bond feature matrix""" |
| edge_index: Tensor = field(init=False) |
| """an tensor of shape ``2 x E`` containing the edges of the graph in COO format""" |
| rev_edge_index: Tensor = field(init=False) |
| """A tensor of shape ``E`` that maps from an edge index to the index of the source of the |
| reverse edge in the ``edge_index`` attribute.""" |
| batch: Tensor = field(init=False) |
| """the index of the parent :class:`MolGraph` in the batched graph""" |
|
|
| __size: int = field(init=False) |
|
|
| def __post_init__(self, mgs: Sequence['MolGraph']): |
| self.__size = len(mgs) |
|
|
| |
| |
| Vs = [mg.V for mg in mgs] |
| Es = [mg.E for mg in mgs] |
| |
| |
| |
| v_lengths = [len(v) for v in Vs] |
| |
| |
| |
| batch_idx_np = np.repeat(np.arange(self.__size), v_lengths) |
| self.batch = torch.as_tensor(batch_idx_np, dtype=torch.long) |
|
|
| |
| |
| |
| node_counts = np.array(v_lengths) |
| node_offsets = np.cumsum(np.concatenate(([0], node_counts[:-1]))) |
| |
| |
| edge_counts = np.array([mg.edge_index.shape[1] for mg in mgs]) |
| edge_offsets = np.cumsum(np.concatenate(([0], edge_counts[:-1]))) |
|
|
| |
| |
| edge_indexes = [mg.edge_index + off for mg, off in zip(mgs, node_offsets)] |
| rev_edge_indexes = [mg.rev_edge_index + off for mg, off in zip(mgs, edge_offsets)] |
|
|
| |
| |
| self.V = torch.as_tensor(np.concatenate(Vs), dtype=torch.float32) |
| self.E = torch.as_tensor(np.concatenate(Es), dtype=torch.float32) |
| |
| |
| self.edge_index = torch.as_tensor(np.hstack(edge_indexes), dtype=torch.long) |
| self.rev_edge_index = torch.as_tensor(np.concatenate(rev_edge_indexes), dtype=torch.long) |
|
|
| def __len__(self) -> int: |
| return self.__size |
|
|
| def to(self, device: str | torch.device): |
| self.V = self.V.to(device) |
| self.E = self.E.to(device) |
| self.edge_index = self.edge_index.to(device) |
| self.rev_edge_index = self.rev_edge_index.to(device) |
| self.batch = self.batch.to(device) |
| return self |
| |
| @classmethod |
| def from_tensors(cls, V, E, edge_index, rev_edge_index, batch, size): |
| """Reconstructs the object from raw tensors (used after collation).""" |
| |
| obj = cls.__new__(cls) |
| |
| |
| obj.V = V |
| obj.E = E |
| obj.edge_index = edge_index |
| obj.rev_edge_index = rev_edge_index |
| obj.batch = batch |
| obj._BatchMolGraph__size = size |
| |
| return obj |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
| |
| |
| |
|
|
| class TrainingBatch(NamedTuple): |
| """ |
| A NamedTuple for a batch of data. |
| |
| Includes the batched molecule graph, feature tensors, target tensors, |
| and the SMILES strings for the molecules in the batch |
| """ |
| bmg: BatchMolGraph |
| V_d: Tensor | None |
| X_d: Tensor | None |
| Y: Tensor | None |
| w: Tensor |
| lt_mask: Tensor | None |
| gt_mask: Tensor | None |
| smiles: Sequence[str] |
| rdkit: Tensor | None |
| teacher_embs: list[Tensor] | None |
| |
|
|
|
|
| def collate_batch(batch: Iterable[Datum]) -> dict: |
|
|
| |
| mgs, V_ds, x_ds, ys, weights, lt_masks, gt_masks, smiles, rdkit, teacher_embs = zip(*batch) |
| |
|
|
|
|
| |
| teacher_embs_list = None |
| if teacher_embs[0] is not None: |
| teacher_embs_list = [ |
| torch.as_tensor(np.stack(col), dtype=torch.float32) |
| for col in zip(*teacher_embs) |
| ] |
|
|
| |
| |
| |
| |
| Vs = [mg.V for mg in mgs] |
| Es = [mg.E for mg in mgs] |
| batch_size = len(mgs) |
|
|
| |
| |
| v_lengths = [len(v) for v in Vs] |
| |
| |
| batch_idx_np = np.repeat(np.arange(batch_size), v_lengths) |
| |
| |
| |
| node_counts = np.array(v_lengths) |
| node_offsets = np.cumsum(np.concatenate(([0], node_counts[:-1]))) |
| |
| |
| edge_counts = np.array([mg.edge_index.shape[1] for mg in mgs]) |
| edge_offsets = np.cumsum(np.concatenate(([0], edge_counts[:-1]))) |
|
|
| |
| |
| edge_indexes = [mg.edge_index + off for mg, off in zip(mgs, node_offsets)] |
| rev_edge_indexes = [mg.rev_edge_index + off for mg, off in zip(mgs, edge_offsets)] |
|
|
| |
| |
| graph_V = torch.as_tensor(np.concatenate(Vs), dtype=torch.float32) |
| graph_E = torch.as_tensor(np.concatenate(Es), dtype=torch.float32) |
| graph_edge_index = torch.as_tensor(np.hstack(edge_indexes), dtype=torch.long) |
| graph_rev_edge_index = torch.as_tensor(np.concatenate(rev_edge_indexes), dtype=torch.long) |
| graph_batch = torch.as_tensor(batch_idx_np, dtype=torch.long) |
|
|
|
|
|
|
| return { |
| |
| "graph_V": graph_V, |
| "graph_E": graph_E, |
| "graph_edge_index": graph_edge_index, |
| "graph_rev_edge_index": graph_rev_edge_index, |
| "graph_batch": graph_batch, |
| "graph_size": batch_size, |
|
|
| |
| "V_ds": None if V_ds[0] is None else torch.as_tensor(np.concatenate(V_ds), dtype=torch.float32), |
| "X_d": None if x_ds[0] is None else torch.as_tensor(np.stack(x_ds), dtype=torch.float32), |
| "Y": None if ys[0] is None else torch.as_tensor(np.stack(ys), dtype=torch.float32), |
| "weights": torch.as_tensor(weights, dtype=torch.float32).unsqueeze(1), |
| "lt_mask": None if lt_masks[0] is None else torch.as_tensor(np.stack(lt_masks)), |
| "gt_mask": None if gt_masks[0] is None else torch.as_tensor(np.stack(gt_masks)), |
| "smiles": None if smiles[0] is None else smiles, |
| "rdkit": None if rdkit[0] is None else torch.as_tensor(np.vstack(rdkit), dtype=torch.float32), |
| "teacher_embs": None if teacher_embs_list is None else teacher_embs_list |
| } |
| |
|
|
|
|
| class MolAtomBondTrainingBatch(NamedTuple): |
| """ |
| A NamedTuple for a batch of data for atom/bond-level tasks. |
| |
| Includes attributes for atom and bond predictions, and the SMILES strings |
| """ |
| bmg: BatchMolAtomBondGraph |
| V_d: Tensor | None |
| E_d: Tensor | None |
| X_d: Tensor | None |
| Ys: tuple[Tensor | None, Tensor | None, Tensor | None] |
| w: tuple[Tensor | None, Tensor | None, Tensor | None] |
| lt_masks: tuple[Tensor | None, Tensor | None, Tensor | None] |
| gt_masks: tuple[Tensor | None, Tensor | None, Tensor | None] |
| constraints: tuple[Tensor | None, Tensor | None] |
| smiles: Sequence[str] |
| rdkit: Tensor | None |
| teacher_embs_tensor : list[Tensor] | None |
|
|
|
|
|
|
| def collate_mol_atom_bond_batch(batch: Iterable[MolAtomBondDatum]) -> MolAtomBondTrainingBatch: |
| """ |
| Collates a batch of MolAtomBondDatum objects into a MolAtomBondTrainingBatch |
| |
| Modified to handle SMILES strings |
| """ |
| mgs, V_ds, E_ds, x_ds, yss, weights, lt_maskss, gt_maskss, constraintss, smiles, rdkit, teacher_embs = zip(*batch) |
|
|
| |
| if teacher_embs[0] is None: |
| teacher_embs_list = None |
| else: |
| num_teachers = len(teacher_embs[0]) |
| teacher_embs_list = [] |
| for i in range(num_teachers): |
| single_teacher_batch = [sample[i] for sample in teacher_embs] |
| tensor = torch.from_numpy(np.stack(single_teacher_batch)).float() |
| teacher_embs_list.append(tensor) |
|
|
|
|
| weights = torch.tensor(weights, dtype=torch.float).unsqueeze(1) |
| weights_tensors = [] |
| for ys in zip(*yss): |
| if ys[0] is None: |
| weights_tensors.append(None) |
| elif ys[0].ndim == 1: |
| weights_tensors.append(weights) |
| else: |
| repeats = torch.tensor([y.shape[0] for y in ys]) |
| weights_tensors.append(torch.repeat_interleave(weights, repeats, dim=0)) |
|
|
| if constraintss[0][0] is None and constraintss[0][1] is None: |
| constraintss = None |
| else: |
| constraintss = [ |
| None if constraints[0] is None else torch.from_numpy(np.array(constraints)).float() |
| for constraints in zip(*constraintss) |
| ] |
|
|
| return MolAtomBondTrainingBatch( |
| BatchMolAtomBondGraph(mgs), |
| None if V_ds[0] is None else torch.from_numpy(np.concatenate(V_ds)).float(), |
| None if E_ds[0] is None else torch.from_numpy(np.concatenate(E_ds)).float(), |
| None if x_ds[0] is None else torch.from_numpy(np.array(x_ds)).float(), |
| [None if ys[0] is None else torch.from_numpy(np.vstack(ys)).float() for ys in zip(*yss)], |
| weights_tensors, |
| [ |
| None if lt_masks[0] is None else torch.from_numpy(np.vstack(lt_masks)) |
| for lt_masks in zip(*lt_maskss) |
| ], |
| [ |
| None if gt_masks[0] is None else torch.from_numpy(np.vstack(gt_masks)) |
| for gt_masks in zip(*gt_maskss) |
| ], |
| constraintss, |
| smiles, |
| None if rdkit[0] is None else torch.from_numpy(np.vstack(rdkit)).float(), |
| teacher_embs_list |
| ) |
|
|
|
|
| class MulticomponentTrainingBatch(NamedTuple): |
| """ |
| A NamedTuple for a batch of multicomponent data |
| |
| Modified to include a list of SMILES for each component |
| """ |
| bmgs: list[BatchMolGraph] |
| V_ds: list[Tensor | None] |
| X_d: Tensor | None |
| Y: Tensor | None |
| w: Tensor |
| lt_mask: Tensor | None |
| gt_mask: Tensor | None |
| smiles: List[Sequence[str]] |
| rdkit: Tensor | None |
| teacher_embs: List[List[Tensor] | None] |
|
|
|
|
| def collate_multicomponent(batches: Iterable[Iterable[Datum]]) -> MulticomponentTrainingBatch: |
| """ |
| Collates a batch of multicomponent data |
| |
| Modified to aggregate the SMILES strings from each component's batch |
| """ |
| tbs = [collate_batch(batch) for batch in zip(*batches)] |
|
|
| return MulticomponentTrainingBatch( |
| [tb.bmg for tb in tbs], |
| [tb.V_d for tb in tbs], |
| tbs[0].X_d, |
| tbs[0].Y, |
| tbs[0].w, |
| tbs[0].lt_mask, |
| tbs[0].gt_mask, |
| [tb.smiles for tb in tbs], |
| tbs[0].rdkit, |
| [tb.teacher_embs for tb in tbs] |
| ) |
|
|
|
|
|
|
| def build_dataloader( |
| dataset: MoleculeDataset | MolAtomBondDataset | ReactionDataset | MulticomponentDataset, |
| batch_size: int = 1024, |
| num_workers: int = 0, |
| shuffle: bool = True, |
| **kwargs, |
| ): |
| """ Build a dataloader given MolGraphDataset |
| |
| Args |
| dataset : MoleculeDataset | ReactionDataset | MulticomponentDataset |
| The dataset containing the molecules or reactions to load. |
| batch_size : int, default=64 |
| the batch size to load. |
| num_workers : int, default=0 |
| the number of workers used to build batches. |
| |
| Returns |
| a torch.utils.data.DataLoader |
| |
| modified from source: https://github.com/chemprop/chemprop/blob/420b18834b81aac48963cccdef04e500f1dd0160/chemprop/data/dataloader.py#L16 |
| """ |
|
|
| if isinstance(dataset, MulticomponentDataset): |
| collate_fn = collate_multicomponent |
| elif isinstance(dataset, MolAtomBondDataset): |
| collate_fn = collate_mol_atom_bond_batch |
| else: |
| collate_fn = collate_batch |
|
|
|
|
| persistent_workers = False |
| pin_memory = False |
| if num_workers > 0: |
| persistent_workers = True |
| pin_memory = True |
|
|
| return DataLoader( |
| dataset, |
| batch_size, |
| shuffle=shuffle, |
| num_workers=num_workers, |
| collate_fn=collate_fn, |
| persistent_workers=persistent_workers, |
| pin_memory=pin_memory, |
| **kwargs, |
| ) |
|
|
|
|
| def randomize_smiles(smiles: str) -> str: |
| """Returns a random valid SMILES string for the same molecule. |
| so instead of taking the canonical smiles, you take an equivalent representation """ |
| mol = Chem.MolFromSmiles(smiles) |
| if mol is None: |
| return smiles |
| |
| return Chem.MolToSmiles(mol, doRandom=True, canonical=False) |
|
|
|
|
| |
|
|
|
|