ATOM_LIST = ['C', 'N', 'O', 'S', 'F', 'Si', 'P', 'Cl', 'Br', 'Mg', 'Na', 'Ca', 'Fe',
'As', 'I', 'B', 'V', 'K', 'Tl', 'Sn', 'Sb', 'Se', 'Zn', 'Other']
HYBRIDIZATIONS = [Chem.rdchem.HybridizationType.SP, Chem.rdchem.HybridizationType.SP2,
Chem.rdchem.HybridizationType.SP3, Chem.rdchem.HybridizationType.SP3D,
Chem.rdchem.HybridizationType.SP3D2, 'Other']
CHIRAL_TAGS = [Chem.rdchem.ChiralType.CHI_UNSPECIFIED, Chem.rdchem.ChiralType.CHI_TETRAHEDRAL_CW,
Chem.rdchem.ChiralType.CHI_TETRAHEDRAL_CCW, 'Other']
BOND_TYPES = [Chem.rdchem.BondType.SINGLE, Chem.rdchem.BondType.DOUBLE,
Chem.rdchem.BondType.TRIPLE, Chem.rdchem.BondType.AROMATIC]
STEREO_TYPES = [Chem.rdchem.BondStereo.STEREONONE, Chem.rdchem.BondStereo.STEREOZ,
Chem.rdchem.BondStereo.STEREOE, 'Other']
def one_hot(x, choices: list) -> list[int]:
v = [0] * len(choices)
idx = choices.index(x) if x in choices else len(choices) - 1
v[idx] = 1
return v
def atom_features(atom: Chem.rdchem.Atom) -> np.ndarray:
"""
Returns a bit vector of atom features.
atom: RDKit atom object (from Mol object)
returns: 1D np.array of atom features with shape (52,)
"""
return np.array(
one_hot(atom.GetSymbol(), ATOM_LIST) +
one_hot(atom.GetTotalDegree(), [0, 1, 2, 3, 4, 5]) +
one_hot(atom.GetFormalCharge(), [-2, -1, 0, 1, 2]) +
one_hot(atom.GetHybridization(), HYBRIDIZATIONS) +
one_hot(atom.GetTotalNumHs(), [0, 1, 2, 3, 4]) +
one_hot(atom.GetChiralTag(), CHIRAL_TAGS) +
[int(atom.GetIsAromatic()), int(atom.IsInRing())],
dtype=np.float32,
)
def bond_features(bond: Chem.rdchem.Bond) -> np.ndarray:
"""
Returns a bit vector of bond features.
bond: RDKit bond object (from BondList object)
returns: 1D np.array of bond features with shape (10,)
"""
return np.array(
one_hot(bond.GetBondType(), BOND_TYPES) +
one_hot(bond.GetStereo(), STEREO_TYPES) +
[int(bond.GetIsConjugated()), int(bond.IsInRing())],
dtype=np.float32,
)
def mol_to_pyg_data(smiles: str,
y: np.ndarray | None = None,
w: np.ndarray | None = None
) -> Data | None:
"""
Converts a SMILES string into a PyTorch Geometric Data object.
Args:
smiles: The SMILES string of the molecule.
y: Optional target labels (e.g., toxicity values) for the molecule.
w: Optional weights/mask for the target labels (1 for present, 0 for missing).
Returns:
A PyTorch Geometric Data object representing the molecule, or None if the SMILES
string cannot be parsed or results in a molecule with no atoms.
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None or mol.GetNumAtoms() == 0:
return None
x = torch.tensor(np.stack([atom_features(a) for a in mol.GetAtoms()]), dtype=torch.float)
# x is node features of shape: (num_nodes_in_molecule, num_node_features)
edge_index, edge_attr = [], []
for bond in mol.GetBonds():
i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
bf = bond_features(bond)
edge_index += [[i, j], [j, i]]
edge_attr += [bf, bf]
if len(edge_index) == 0: # single-atom molecule edge case, no bonds (e.g. bare ions like [Na+])
edge_index = torch.empty((2, 0), dtype=torch.long)
# PyG expects shape (2, num_edges), so empty (2, 0) is start/end nodes with no edges
# torch.long gives int64
edge_attr = torch.empty((0, BOND_FEAT_DIM), dtype=torch.float)
# Gives empty edge_attr shape (0, 10)
else:
edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
# If a atom has N bonds -> edge index will be: (2, 2 * N), which is:
# (StartAtom/EndAtom, i:j/j:i * N bonds)
edge_attr = torch.tensor(np.stack(edge_attr), dtype=torch.float)
# edge_attr output: (2 * N bonds, 10)
"""
Demo:
edge_index = [[1, 2], [2, 1], [3, 4], [4, 3]]
In [29]: torch.tensor(edge_index, dtype=torch.long)
Out[29]:
tensor([[1, 2],
[2, 1],
[3, 4],
[4, 3]])
In [30]: torch.tensor(edge_index, dtype=torch.long).t()
Out[30]:
tensor([[1, 2, 3, 4],
[2, 1, 4, 3]])
"""
# PyG Data object aggregates all the information for a single graph into one container
data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, smiles=smiles)
if y is not None:
data.y = torch.tensor(y, dtype=torch.float).view(1, -1)
data.mask = torch.tensor(w, dtype=torch.float).view(1, -1)
return data
ATOM_FEAT_DIM = len(atom_features(Chem.MolFromSmiles("CCO").GetAtomWithIdx(0)))
BOND_FEAT_DIM = len(bond_features(Chem.MolFromSmiles("CCO").GetBondWithIdx(0)))
print("atom feature dim:", ATOM_FEAT_DIM, "| bond feature dim:", BOND_FEAT_DIM)