HYG-mol: An Interpretable Multimodal Hypergraph Framework for Molecular Property Prediction.
The 11 matches
- [1] § Results › Experimental setting ↔ src/main.py, lines 20–74 · score 0.78 · FreeSolv, scaffold splitting, random seeds, stratified, threshold, QM7
- [2] § Methods › Molecular hypergraph construction and representation › Hyperedge representation ↔ src/data/dataset.py, lines 627–719 · score 0.67 · substructure matching, SMARTS patterns, alerts, amides, amines, esters
- [3] § Methods › Molecular hypergraph construction and representation › Multimodal hypernode representation ↔ src/data/dataset.py, lines 137–199 · score 0.66 · Gasteiger charge, formal charge, electrons, traditional, aromaticity, ChemBERTa
- [4] § Methods › Molecular hypergraph construction and representation › Multimodal hypernode representation ↔ src/data/dataset.py, lines 256–316 · score 0.63 · tokens mapped, atom mapping, ChemBERTa model, tokenized, SMILES, mol
- [5] § Results › Experimental setting ↔ src/utils/utils.py, lines 20–79 · score 0.62 · Murcko scaffold splitting, class distribution, threshold, subsets, seeds, 10 %
- [6] § Results › Outlier analysis ↔ src/data/dataset.py, lines 627–719 · score 0.60 · c1ccc2c, OC, cc1, Cl, halogen, motifs
- [7] § Methods › Molecular hypergraph construction and representation › Hyperedge representation ↔ src/data/dataset.py, lines 34–135 · score 0.59 · isolated atoms, ring connections, RDKit, Node features, substructures, hyperedges
- [8] § Results › Interpretability analysis ↔ src/data/dataset.py, lines 759–896 · score 0.56 · carbonyl, donor, bridging, hydrophobic, acceptor, amide
- [9] § Results › Experimental setting ↔ src/training/trainer.py, lines 275–389 · score 0.56 · squared error, absolute error, MAE, RMSE, regression, metrics
- [10] § Results › Comparison experiments ↔ src/main.py, lines 20–74 · score 0.55 · FreeSolv, random seeds, QM7, Lipophilicity, QM8, regression
- [11] § Methods ↔ src/data/dataset.py, lines 34–135 · score 0.51 · ring connections, RDKit, matching, substructures, nodes, Atoms
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 1,787 lines · 72 KB · no license · 7 matches
- # src/data/dataset.py
- import os
- import warnings
- import numpy as np
- import pandas as pd
- import torch
- from .. import config
- from torch_geometric.data import Data, Dataset
- from tqdm import tqdm
- from rdkit import Chem
- from rdkit.Chem import Draw, AllChem, rdMolDescriptors, Descriptors, Crippen, MolSurf
- from rdkit.Chem.Scaffolds import MurckoScaffold
- from transformers import AutoTokenizer, AutoModel
- from rdkit import RDLogger
- RDLogger.DisableLog('rdApp.*')
- class MoleculeHypergraph:
- def __init__(self, feature_type):
- self.feature_type = feature_type
- self.node_features = []
- self.hyperedge_index = None
- self.mol = None
- self.hyperedges = []
- self.hyperedge_labels = []
- if self.feature_type != "traditional_only":
- self.tokenizer = AutoTokenizer.from_pretrained(config.CHEMBERTA_MODEL_NAME)
- self.chemberta_model = AutoModel.from_pretrained(config.CHEMBERTA_MODEL_NAME).to('cpu')
- else:
- self.tokenizer = None
- self.chemberta_model = None
- def build_from_mol(self, mol):
- self.mol = mol
- num_atoms = mol.GetNumAtoms()
- functional_groups, fg_labels = self.get_functional_groups(mol)
- rings, ring_labels = self.get_ring_structures(mol)
- special_structures, special_labels = self.get_special_structures(mol)
- hyperedges = []
- hyperedge_labels = []
- def add_valid_hyperedge(edge, label):
- valid_nodes = [node_id for node_id in edge if 0 <= node_id < num_atoms]
- if valid_nodes:
- is_duplicate = False
- sorted_valid_nodes_tuple = tuple(sorted(valid_nodes))
- for existing_edge_tuple, existing_label in zip(map(lambda e: tuple(sorted(e)), hyperedges),
- hyperedge_labels):
- if existing_edge_tuple == sorted_valid_nodes_tuple and existing_label == label:
- is_duplicate = True
- break
- if not is_duplicate:
- hyperedges.append(valid_nodes)
- hyperedge_labels.append(label)
- for edge, label in zip(functional_groups, fg_labels):
- add_valid_hyperedge(edge, label)
- for edge, label in zip(rings, ring_labels):
- add_valid_hyperedge(edge, label)
- for edge, label in zip(special_structures, special_labels):
- add_valid_hyperedge(edge, label)
- ring_atoms = set()
- atom_to_ring = {}
- for ring_id, ring_node_indices in enumerate(rings):
- ring_atoms.update(ring_node_indices)
- for atom_idx in ring_node_indices:
- atom_to_ring.setdefault(atom_idx, set()).add(ring_id)
- for bond in mol.GetBonds():
- begin_idx = bond.GetBeginAtom().GetIdx()
- end_idx = bond.GetEndAtom().GetIdx()
- if begin_idx >= num_atoms or end_idx >= num_atoms:
- continue
- begin_in_ring = begin_idx in ring_atoms
- end_in_ring = end_idx in ring_atoms
- if begin_in_ring and end_in_ring:
- common_rings = atom_to_ring.get(begin_idx, set()) & atom_to_ring.get(end_idx, set())
- if not common_rings:
- add_valid_hyperedge([begin_idx, end_idx], 'RingConnector')
- elif begin_in_ring != end_in_ring:
- add_valid_hyperedge([begin_idx, end_idx], 'RingSubstituent')
- all_atoms_in_hyperedges = set()
- for he in hyperedges:
- all_atoms_in_hyperedges.update(he)
- excluded_atoms = set(range(num_atoms)) - all_atoms_in_hyperedges
- for atom_idx in list(excluded_atoms):
- atom = mol.GetAtomWithIdx(atom_idx)
- neighbor_indices = [n.GetIdx() for n in atom.GetNeighbors() if n.GetIdx() < num_atoms]
- if neighbor_indices:
- current_group = sorted([atom_idx] + neighbor_indices)
- is_new_group = True
- for existing_edge, existing_label in zip(hyperedges, hyperedge_labels):
- if existing_label == 'IsolatedAtomGroup' and sorted(existing_edge) == current_group:
- is_new_group = False
- break
- if is_new_group:
- add_valid_hyperedge(current_group, 'IsolatedAtomGroup')
- else:
- add_valid_hyperedge([atom_idx], 'SingleAtom')
- try:
- murcko_mol_full = MurckoScaffold.GetScaffoldForMol(mol)
- if murcko_mol_full.GetNumAtoms() > 0:
- match_indices_full = mol.GetSubstructMatch(murcko_mol_full)
- if match_indices_full:
- add_valid_hyperedge(list(match_indices_full), 'MurckoScaffoldFull')
- if murcko_mol_full.GetNumAtoms() > 0:
- murcko_mol_generic = MurckoScaffold.MakeScaffoldGeneric(murcko_mol_full)
- if murcko_mol_generic.GetNumAtoms() > 0:
- match_indices_generic = mol.GetSubstructMatch(murcko_mol_generic)
- if match_indices_generic:
- add_valid_hyperedge(list(match_indices_generic), 'MurckoScaffoldCore')
- except ImportError:
- print(
- "Warning: RDKit MurckoScaffold module not available. Skipping Murcko scaffold hyperedges.")
- except Exception as e:
- print(f"Warning: Error processing Murcko scaffold for molecule {Chem.MolToSmiles(mol)}: {e}")
- self.build_node_features(mol)
- self.build_hyperedge_index(hyperedges, hyperedge_labels)
- return self
- def build_node_features(self, mol):
- num_atoms = mol.GetNumAtoms()
- model_max_length = 512
- if hasattr(self.chemberta_model, 'config') and hasattr(self.chemberta_model.config, 'max_position_embeddings'):
- model_max_length = self.chemberta_model.config.max_position_embeddings
- mol_id = Chem.MolToSmiles(mol)
- cache_key = f"{self.feature_type}_{mol_id}"
- if hasattr(self, 'feature_cache') and cache_key in self.feature_cache:
- self.node_features = self.feature_cache[cache_key]
- return self.node_features
- if not hasattr(self, 'feature_cache'):
- self.feature_cache = {}
- if self.feature_type == "traditional_only":
- additional_features = []
- mol_weight = Descriptors.MolWt(mol) / 500.0 if hasattr(Descriptors, 'MolWt') else 0.0
- logp = Crippen.MolLogP(mol) / 10.0 if hasattr(Crippen, 'MolLogP') else 0.0
- tpsa = MolSurf.TPSA(mol) / 100.0 if hasattr(MolSurf, 'TPSA') else 0.0
- num_rings = len(mol.GetRingInfo().AtomRings()) / 10.0 if hasattr(mol, 'GetRingInfo') else 0.0
- charges = [0.0] * num_atoms
- try:
- AllChem.ComputeGasteigerCharges(mol)
- charges = [atom.GetDoubleProp('_GasteigerCharge')
- if atom.HasProp('_GasteigerCharge') else 0.0
- for atom in mol.GetAtoms()]
- charges = [0.0 if (c is None or np.isnan(c) or np.isinf(c)) else c for c in charges]
- except:
- pass
- ring_info = mol.GetRingInfo()
- for atom_idx in range(num_atoms):
- atom = mol.GetAtomWithIdx(atom_idx)
- neighbors = [n.GetIdx() for n in atom.GetNeighbors()]
- base_features = [
- atom.GetAtomicNum() / 100.0,
- atom.GetDegree() / 4.0,
- int(atom.GetIsAromatic()),
- atom.GetFormalCharge() / 8.0,
- atom.GetNumRadicalElectrons() / 8.0,
- atom.GetChiralTag() / 10.0,
- atom.GetHybridization() / 6.0,
- atom.GetImplicitValence() / 8.0,
- atom.IsInRing() * 1.0,
- len(neighbors) / 8.0,
- ]
- hybridization_features = [
- int(str(atom.GetHybridization()) == "SP"),
- int(str(atom.GetHybridization()) == "SP2"),
- int(str(atom.GetHybridization()) == "SP3"),
- ]
- atom_type_features = [
- int(atom.GetAtomicNum() == 1), # H
- int(atom.GetAtomicNum() == 6), # C
- int(atom.GetAtomicNum() == 7), # N
- int(atom.GetAtomicNum() == 8), # O
- int(atom.GetAtomicNum() == 9), # F
- int(atom.GetAtomicNum() == 15), # P
- int(atom.GetAtomicNum() == 16), # S
- int(atom.GetAtomicNum() == 17), # Cl
- int(atom.GetAtomicNum() == 35), # Br
- int(atom.GetAtomicNum() == 53), # I
- ]
- bond_types = [0, 0, 0, 0]
- for neighbor in neighbors:
- bond = mol.GetBondBetweenAtoms(atom_idx, neighbor)
- if bond.GetBondType() == Chem.rdchem.BondType.SINGLE:
- bond_types[0] += 1
- elif bond.GetBondType() == Chem.rdchem.BondType.DOUBLE:
- bond_types[1] += 1
- elif bond.GetBondType() == Chem.rdchem.BondType.TRIPLE:
- bond_types[2] += 1
- elif bond.GetBondType() == Chem.rdchem.BondType.AROMATIC:
- bond_types[3] += 1
- if sum(bond_types) > 0:
- bond_types = [b / sum(bond_types) for b in bond_types]
- neighbor_features = []
- if neighbors:
- neighbor_atomic_nums = [mol.GetAtomWithIdx(n).GetAtomicNum() for n in neighbors]
- neighbor_features = [
- sum(1 for n in neighbor_atomic_nums if n == 6) / len(neighbors),
- sum(1 for n in neighbor_atomic_nums if n == 7) / len(neighbors),
- sum(1 for n in neighbor_atomic_nums if n == 8) / len(neighbors),
- sum(1 for n in neighbor_atomic_nums if n == 9 or n == 17 or n == 35 or n == 53) / len(neighbors)
- ]
- else:
- neighbor_features = [0.0] * 4
- additional_chemical_features = [
- charges[atom_idx],
- atom.GetTotalNumHs() / 4.0,
- int(ring_info.IsAtomInRingOfSize(atom_idx, 3)),
- int(ring_info.IsAtomInRingOfSize(atom_idx, 5)),
- int(ring_info.IsAtomInRingOfSize(atom_idx, 6)),
- ]
- global_mol_features = [mol_weight, logp, tpsa, num_rings]
- atom_features = (
- base_features +
- hybridization_features +
- atom_type_features +
- bond_types +
- neighbor_features +
- additional_chemical_features +
- global_mol_features
- )
- additional_features.append(atom_features)
- self.node_features = np.array(additional_features, dtype=np.float32)
- elif self.feature_type == "chemberta_only":
- original_smiles = Chem.MolToSmiles(mol)
- atom_mapped_mol = Chem.AddHs(mol)
- for atom in atom_mapped_mol.GetAtoms():
- atom.SetAtomMapNum(atom.GetIdx() + 1)
- inputs = self.tokenizer(
- original_smiles,
- return_tensors="pt",
- truncation=True,
- max_length=model_max_length,
- padding="max_length"
- )
- inputs = {k: v.to('cpu') for k, v in inputs.items()}
- with torch.no_grad():
- outputs = self.chemberta_model(**inputs)
- hidden_states = outputs.last_hidden_state.squeeze(0)
- tokens = self.tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
- token_to_char = {}
- smiles_tokens = self.tokenizer.tokenize(original_smiles)
- current_pos = 0
- for i, token in enumerate(smiles_tokens):
- token_to_char[i] = current_pos
- clean_token = token.replace('#', '').replace('Ġ', '')
- current_pos += len(clean_token)
- token_to_atom_map = {}
- current_atom_idx = 0
- atom_to_token_map = {}
- for token_idx, token in enumerate(tokens):
- if token == self.tokenizer.cls_token or \
- token == self.tokenizer.sep_token or \
- token == self.tokenizer.pad_token:
- continue
- if token.startswith('##'):
- if token_idx > 0 and token_idx - 1 in token_to_atom_map:
- token_to_atom_map[token_idx] = token_to_atom_map[token_idx - 1]
- continue
- atom_tokens = ['C', 'c', 'N', 'n', 'O', 'o', 'S', 's', 'P', 'p', 'F', 'Cl', 'Br', 'I',
- '[cH]', '[nH]', '[oH]', '[sH]', '[CH]', '[CH2]', '[CH3]', '[NH]', '[NH2]',
- '[OH]', '[SH]', '[PH]']
- if any(token == t or token.startswith(t) for t in atom_tokens):
- if current_atom_idx < num_atoms:
- token_to_atom_map[token_idx] = current_atom_idx
- if current_atom_idx not in atom_to_token_map:
- atom_to_token_map[current_atom_idx] = []
- atom_to_token_map[current_atom_idx].append(token_idx)
- current_atom_idx += 1
- else:
- break
- feature_dim = hidden_states.shape[1]
- atom_features = torch.zeros((num_atoms, feature_dim), device='cpu')
- mapped_atom_indices = set()
- for atom_idx, token_indices in atom_to_token_map.items():
- if atom_idx < num_atoms:
- valid_tokens = [ti for ti in token_indices if ti < hidden_states.shape[0]]
- if valid_tokens:
- weights = torch.ones(len(valid_tokens))
- for i, ti in enumerate(valid_tokens):
- if not tokens[ti].startswith('##'):
- weights[i] = 2.0
- weights = weights / weights.sum()
- for i, ti in enumerate(valid_tokens):
- atom_features[atom_idx] += weights[i] * hidden_states[ti]
- mapped_atom_indices.add(atom_idx)
- else:
- print(f"Warning: No valid token indices for atom {atom_idx}")
- for atom_idx in range(num_atoms):
- if atom_idx not in mapped_atom_indices:
- atom = mol.GetAtomWithIdx(atom_idx)
- neighbors = [n.GetIdx() for n in atom.GetNeighbors() if n.GetIdx() in mapped_atom_indices]
- if neighbors:
- neighbor_features = atom_features[neighbors]
- atom_features[atom_idx] = torch.mean(neighbor_features, dim=0)
- mapped_atom_indices.add(atom_idx)
- unmapped_atoms = [i for i in range(num_atoms) if i not in mapped_atom_indices]
- if unmapped_atoms:
- valid_token_indices = [
- idx for idx, t in enumerate(tokens)
- if t != self.tokenizer.cls_token and \
- t != self.tokenizer.sep_token and \
- t != self.tokenizer.pad_token and \
- idx < hidden_states.shape[0]
- ]
- if valid_token_indices:
- global_features = torch.mean(hidden_states[valid_token_indices], dim=0)
- for atom_idx in unmapped_atoms:
- atom_features[atom_idx] = global_features
- else:
- print(f"Warning: No valid token features for global average in SMILES: {original_smiles[:50]}...")
- self.node_features = atom_features.cpu().numpy()
- else:
- original_smiles = Chem.MolToSmiles(mol)
- atom_mapped_mol = Chem.AddHs(mol)
- for atom in atom_mapped_mol.GetAtoms():
- atom.SetAtomMapNum(atom.GetIdx() + 1)
- inputs = self.tokenizer(
- original_smiles,
- return_tensors="pt",
- truncation=True,
- max_length=model_max_length,
- padding="max_length"
- )
- inputs = {k: v.to('cpu') for k, v in inputs.items()}
- with torch.no_grad():
- outputs = self.chemberta_model(**inputs)
- hidden_states = outputs.last_hidden_state.squeeze(0)
- tokens = self.tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
- token_to_atom_map = {}
- atom_to_token_map = {}
- current_atom_idx = 0
- for token_idx, token in enumerate(tokens):
- if token == self.tokenizer.cls_token or \
- token == self.tokenizer.sep_token or \
- token == self.tokenizer.pad_token:
- continue
- if token.startswith('##'):
- if token_idx > 0 and token_idx - 1 in token_to_atom_map:
- token_to_atom_map[token_idx] = token_to_atom_map[token_idx - 1]
- continue
- atom_tokens = ['C', 'c', 'N', 'n', 'O', 'o', 'S', 's', 'P', 'p', 'F', 'Cl', 'Br', 'I',
- '[cH]', '[nH]', '[oH]', '[sH]']
- if any(token == t or token.startswith(t) for t in atom_tokens):
- if current_atom_idx < num_atoms:
- token_to_atom_map[token_idx] = current_atom_idx
- if current_atom_idx not in atom_to_token_map:
- atom_to_token_map[current_atom_idx] = []
- atom_to_token_map[current_atom_idx].append(token_idx)
- current_atom_idx += 1
- else:
- break
- feature_dim = hidden_states.shape[1]
- atom_features_tensor = torch.zeros((num_atoms, feature_dim), device='cpu')
- mapped_atom_indices = set()
- for atom_idx, token_indices in atom_to_token_map.items():
- if atom_idx < num_atoms:
- valid_tokens = [ti for ti in token_indices if ti < hidden_states.shape[0]]
- if valid_tokens:
- for ti in valid_tokens:
- atom_features_tensor[atom_idx] += hidden_states[ti]
- atom_features_tensor[atom_idx] /= len(valid_tokens)
- mapped_atom_indices.add(atom_idx)
- for atom_idx in range(num_atoms):
- if atom_idx not in mapped_atom_indices:
- atom = mol.GetAtomWithIdx(atom_idx)
- neighbors = [n.GetIdx() for n in atom.GetNeighbors() if n.GetIdx() in mapped_atom_indices]
- if neighbors:
- neighbor_features = atom_features_tensor[neighbors]
- atom_features_tensor[atom_idx] = torch.mean(neighbor_features, dim=0)
- mapped_atom_indices.add(atom_idx)
- else:
- valid_token_indices = [
- idx for idx, t in enumerate(tokens)
- if t != self.tokenizer.cls_token and \
- t != self.tokenizer.sep_token and \
- t != self.tokenizer.pad_token and \
- idx < hidden_states.shape[0]
- ]
- if valid_token_indices:
- atom_features_tensor[atom_idx] = torch.mean(hidden_states[valid_token_indices], dim=0)
- else:
- print(
- f"Warning: No valid token features to average for fallback on atom {atom_idx} in SMILES: {original_smiles[:50]}...")
- mol_weight = Descriptors.MolWt(mol) / 500.0 if hasattr(Descriptors, 'MolWt') else 0.0
- logp = Crippen.MolLogP(mol) / 10.0 if hasattr(Crippen, 'MolLogP') else 0.0
- tpsa = MolSurf.TPSA(mol) / 100.0 if hasattr(MolSurf, 'TPSA') else 0.0
- charges = [0.0] * num_atoms
- try:
- AllChem.ComputeGasteigerCharges(mol)
- charges = [atom.GetDoubleProp('_GasteigerCharge')
- if atom.HasProp('_GasteigerCharge') else 0.0
- for atom in mol.GetAtoms()]
- charges = [0.0 if (c is None or np.isnan(c) or np.isinf(c)) else c for c in charges]
- except:
- pass
- additional_features_list = []
- for atom_idx in range(num_atoms):
- atom = mol.GetAtomWithIdx(atom_idx)
- neighbors = [n.GetIdx() for n in atom.GetNeighbors()]
- base_features = [
- atom.GetAtomicNum() / 100.0,
- atom.GetDegree() / 4.0,
- int(atom.GetIsAromatic()),
- atom.GetFormalCharge() / 8.0,
- atom.GetNumRadicalElectrons() / 8.0
- ]
- enhanced_features = [
- charges[atom_idx],
- int(atom.IsInRing()),
- atom.GetTotalNumHs() / 4.0,
- mol_weight,
- logp,
- tpsa
- ]
- atom_additional_features = base_features + enhanced_features
- additional_features_list.append(atom_additional_features)
- additional_features_tensor = torch.tensor(additional_features_list, dtype=torch.float, device='cpu')
- bert_weight = 0.6
- trad_weight = 0.4
- bert_mean = torch.mean(atom_features_tensor, dim=0, keepdim=True)
- bert_std = torch.std(atom_features_tensor, dim=0, keepdim=True) + 1e-8
- atom_features_tensor = (atom_features_tensor - bert_mean) / bert_std
- trad_mean = torch.mean(additional_features_tensor, dim=0, keepdim=True)
- trad_std = torch.std(additional_features_tensor, dim=0, keepdim=True) + 1e-8
- additional_features_tensor = (additional_features_tensor - trad_mean) / trad_std
- atom_features_tensor = bert_weight * atom_features_tensor
- additional_features_tensor = trad_weight * additional_features_tensor
- final_features = torch.cat([atom_features_tensor, additional_features_tensor], dim=1)
- self.node_features = final_features.cpu().numpy()
- if hasattr(self, 'use_nonlinear_features') and self.use_nonlinear_features:
- self.node_features = self._add_nonlinear_interactions(self.node_features)
- self.feature_cache[cache_key] = self.node_features
- assert self.node_features.shape[0] == num_atoms, \
- f"Feature matrix shape {self.node_features.shape} does not match number of atoms {num_atoms}"
- return self.node_features
- def _add_nonlinear_interactions(self, features):
- if features.shape[1] > 30:
- main_features = features[:, :10]
- else:
- main_features = features
- num_samples, num_features = main_features.shape
- interactions = []
- for i in range(min(5, num_features)):
- interactions.append(np.square(main_features[:, i]).reshape(-1, 1))
- count = 0
- for i in range(min(5, num_features)):
- for j in range(i + 1, min(5, num_features)):
- if count < 10:
- interactions.append((main_features[:, i] * main_features[:, j]).reshape(-1, 1))
- count += 1
- if interactions:
- interaction_features = np.hstack(interactions)
- return np.hstack([features, interaction_features])
- else:
- return features
- def build_hyperedge_index(self, hyperedges, hyperedge_labels):
- node_idx = []
- edge_idx = []
- num_nodes = self.mol.GetNumAtoms()
- valid_edges = []
- valid_labels = []
- for edge_id, (hyperedge, label) in enumerate(zip(hyperedges, hyperedge_labels)):
- valid_nodes = [node_id for node_id in hyperedge if 0 <= node_id < num_nodes]
- if len(valid_nodes) > 0:
- if len(valid_nodes) != len(hyperedge):
- print(
- f"Warning: Hyperedge {edge_id} contains invalid nodes, keeping only valid ones: {valid_nodes}")
- valid_edges.append(valid_nodes)
- valid_labels.append(label)
- else:
- print(f"Warning: Hyperedge {edge_id} has no valid nodes, discarding")
- if not valid_edges:
- print("Warning: No valid hyperedges found! Creating a default edge.")
- if num_nodes > 0:
- valid_edges = [[0]]
- valid_labels = ["Default"]
- else:
- raise ValueError("No valid hyperedges could be created for molecule with no atoms")
- self.hyperedges = valid_edges
- self.hyperedge_labels = valid_labels
- for edge_id, hyperedge in enumerate(valid_edges):
- for node_id in hyperedge:
- if 0 <= node_id < num_nodes:
- node_idx.append(node_id)
- edge_idx.append(edge_id)
- if not node_idx or not edge_idx:
- print("Warning: Empty hyperedge index after processing. Creating fallback index.")
- if num_nodes > 0:
- node_idx = [0]
- edge_idx = [0]
- else:
- raise ValueError("Cannot create valid hyperedge index for empty molecule")
- self.hyperedge_index = np.array([node_idx, edge_idx])
- def get_functional_groups(self, mol):
- fg_smarts = {
- 'Amine': '[NX3;H2,H1,H0;!$(NC=O)]',
- 'QuaternaryAmmonium': '[NX4+]',
- 'Alcohol': '[OX2H]',
- 'Phenol': 'c[OH]',
- 'CarboxylicAcid': 'C(=O)[OX1H0-,OX2H1]',
- 'Ester': 'C(=O)O[C;!$(C=O)]',
- 'Ketone': 'C(=O)[C;!$(C=O)]',
- 'Aldehyde': '[CX3H1](=O)[#6]',
- 'Ether': '[OD2]([#6])[#6]',
- 'Amide': 'C(=O)N',
- 'Halogen': '[F,Cl,Br,I]',
- 'Sulfonamide': 'S(=O)(=O)N',
- 'Thiol': '[#16X2H]',
- 'Disulfide': '[#16X2]-[#16X2]',
- 'Nitrate': '[N+](=O)[O-]',
- 'Cyano': '[C]#N',
- 'Azide': '[N]=[N+]=[N-]',
- 'Alkyne': '[CX2]#C',
- 'Alkene': 'C=C',
- 'Phosphate': '[#15](=O)(O)(O)O',
- 'Phosphonate': 'P(=O)(O)(O)[C]',
- 'Sulfate': 'S(=O)(=O)(O)(O)',
- 'Sulfonate': 'S(=O)(=O)O[C]',
- 'Sulfoxide': '[#16X3+1][#6]',
- 'SulfonicAcid': 'S(=O)(=O)[OH]',
- 'Isocyanate': 'N=C=O',
- 'Urea': 'N-C(=O)-N',
- 'Carbamate': 'O=C(O)N',
- 'Imine': '[CX2]=[NX2]',
- 'Thioether': '[#16X2][#6]',
- 'Epoxide': '[C@H1]1O[C@H1]1',
- 'Peroxide': '[OX2][OX2]',
- 'BoronicAcid': 'B(O)O',
- 'Anhydride': 'C(=O)OC(=O)',
- 'Thiocyanate': '[N-]=C=S',
- 'Isothiocyanate': 'N=C=S',
- 'Oxime': '[CX3](=NO)',
- 'Hydrazone': '[CX3](=NN)',
- 'Guanidine': 'N=C(N)N',
- 'Pyridine': 'n1ccccc1',
- 'Pyrazine': 'n1cnccn1',
- 'Pyrrole': 'n1cccc1',
- 'Imidazole': 'n1c[nH]cc1',
- 'Thiazole': 'c1ncsc1',
- 'NitroAromatic': '[N](=O)=O[c]',
- 'Quinone': 'O=C1C=CC(=O)C=C1',
- 'Piperidine': 'N1CCCCC1',
- 'Pyrrolidine': 'N1CCCC1',
- 'Morpholine': 'O1CCNCC1',
- 'TertiaryButylEster': 'C(=O)OC(C)(C)C',
- 'IsopropylEster': 'C(=O)O[CH](C)C',
- 'MethylEster': 'C(=O)OC',
- 'EthylEster': 'C(=O)OCC',
- 'PropylEster': 'C(=O)OCCC',
- 'HexylEster': 'C(=O)OCCCCCC',
- 'OctylEster': 'C(=O)OCCCCCCCC',
- 'Fluorine': '[F]',
- 'Trifluoromethyl': 'C(F)(F)F',
- 'Adamantane': 'C1C2CC3CC(C1)CC(C2)C3',
- 'Cyclohexyl': 'C1CCCCC1',
- 'Sulfone': 'S(=O)(=O)[C,N,O]',
- 'PhosphineOxide': 'P(=O)[C,N,O]',
- 'Benzene': 'c1ccccc1',
- 'Naphthalene': 'c1cccc2ccccc12',
- 'NitroAromatic': '[$(c-[N+](=O)[O-]),$(c-[N+]-[O-])]',
- 'Aniline': 'c-[NX3H2]',
- 'MichaelAcceptor_Enone': '[CX3]=[CX3]-[CX3](=[O,S,N])',
- 'Epoxide_Alert': '[OD1r3]1[#6r3][#6r3]1',
- 'Tetrahydroisoquinoline_Core_Alert': 'c1ccc2c(c1)CCNCC2',
- '4_Phenylpiperidine_Motif': 'c1ccccc1-C1CCNCC1',
- }
- functional_groups = []
- fg_labels = []
- for fg_name, smarts in fg_smarts.items():
- try:
- pattern = Chem.MolFromSmarts(smarts)
- if pattern is None:
- warnings.warn(f"Invalid SMARTS pattern: {fg_name} -> {smarts}")
- continue
- matches = mol.GetSubstructMatches(pattern)
- for match in matches:
- fg_atoms = set(match)
- functional_groups.append(list(fg_atoms))
- fg_labels.append(fg_name)
- except Exception as e:
- warnings.warn(f"Error in matching SMARTS pattern: {fg_name} -> {smarts}: {e}")
- continue
- return functional_groups, fg_labels
- def get_ring_structures(self, mol):
- rings = []
- labels = []
- ring_info = mol.GetRingInfo()
- for idxs in ring_info.AtomRings():
- is_aromatic = all([mol.GetAtomWithIdx(idx).GetIsAromatic() for idx in idxs])
- ring_size = len(idxs)
- ring_label = f"AromaticRing_{ring_size}" if is_aromatic else f"Ring_{ring_size}"
- rings.append(list(idxs))
- labels.append(ring_label)
- fused_rings = rdMolDescriptors.CalcNumSpiroAtoms(mol)
- if fused_rings > 0:
- labels.append("FusedRings")
- rings.append([atom.GetIdx() for atom in mol.GetAtoms() if atom.IsInRing()])
- return rings, labels
- def get_special_structures(self, mol):
- special_structures = []
- special_labels = []
- metal_atomic_numbers = [13, 12, 20, 26, 30, 29, 25, 24, 27, 28, 33, 80, 82, 50]
- metal_atoms = [atom.GetIdx() for atom in mol.GetAtoms() if atom.GetAtomicNum() in metal_atomic_numbers]
- if metal_atoms:
- special_structures.append(metal_atoms)
- special_labels.append("MetalAtoms")
- ssr = Chem.GetSymmSSSR(mol)
- atom_rings = [set(ring) for ring in ssr]
- spiro_atoms = set()
- for i in range(len(atom_rings)):
- for j in range(i + 1, len(atom_rings)):
- shared_atoms = atom_rings[i] & atom_rings[j]
- if len(shared_atoms) == 1:
- spiro_atoms.update(shared_atoms)
- if spiro_atoms:
- special_structures.append(list(spiro_atoms))
- special_labels.append("SpiroAtoms")
- return special_structures, special_labels
- def generate_enhanced_hyperedge_attributes(self):
- num_hyperedges = len(self.hyperedges)
- hyperedge_features = np.zeros((num_hyperedges, 5), dtype=np.float32)
- if num_hyperedges == 0:
- print(f"Warning: No hyperedges found for molecule. Creating default attribute matrix with shape [1, 5].")
- return np.zeros((1, 5), dtype=np.float32)
- for i, (edge, label) in enumerate(zip(self.hyperedges, self.hyperedge_labels)):
- if i >= hyperedge_features.shape[0]:
- print(
- f"Warning: Hyperedge index {i} exceeds feature matrix dimension {hyperedge_features.shape[0]}. Expanding matrix.")
- expanded_features = np.zeros((i + 1, 5), dtype=np.float32)
- expanded_features[:hyperedge_features.shape[0]] = hyperedge_features
- hyperedge_features = expanded_features
- current_label_handled = False
- if 'MurckoScaffoldCore' in label:
- hyperedge_features[i, 0] = 0.95
- current_label_handled = True
- elif 'MurckoScaffoldFull' in label:
- hyperedge_features[i, 0] = 0.90
- current_label_handled = True
- if not current_label_handled:
- if 'Ring' in label:
- if 'Aromatic' in label:
- hyperedge_features[i, 0] = 0.85
- else:
- hyperedge_features[i, 0] = 0.75
- elif any(fg in label for fg in ['Amine', 'Alcohol', 'Acid', 'Amide', 'Carbonyl', 'Ester', 'Ketone', 'Aldehyde']):
- hyperedge_features[i, 0] = 0.80
- elif any(fg in label for fg in ['Halogen', 'Cyano', 'Nitro', 'Sulfonamide', 'Thiol', 'Sulfone']):
- hyperedge_features[i, 0] = 0.70
- elif 'Connector' in label or 'Bridge' in label or 'Substituent' in label:
- hyperedge_features[i, 0] = 0.60
- elif 'IsolatedAtomGroup' in label or 'SingleAtom' in label:
- hyperedge_features[i, 0] = 0.30
- else:
- hyperedge_features[i, 0] = 0.50
- if 'Ring' in label:
- if 'Aromatic' in label:
- hyperedge_features[i, 0] = 0.9
- else:
- hyperedge_features[i, 0] = 0.7
- elif any(fg in label for fg in ['Amine', 'Alcohol', 'Acid', 'Amide', 'Carbonyl']):
- hyperedge_features[i, 0] = 0.8
- elif any(fg in label for fg in ['Halogen', 'Cyano', 'Nitro']):
- hyperedge_features[i, 0] = 0.75
- elif 'Connector' in label or 'Bridge' in label:
- hyperedge_features[i, 0] = 0.6
- else:
- hyperedge_features[i, 0] = 0.5
- hyperedge_size = len(edge)
- normalized_size = min(1.0, hyperedge_size / 10.0)
- hyperedge_features[i, 1] = normalized_size
- electron_feature = 0.0
- atom_count = 0
- for atom_idx in edge:
- if atom_idx < self.mol.GetNumAtoms():
- atom = self.mol.GetAtomWithIdx(atom_idx)
- atomic_num = atom.GetAtomicNum()
- if atomic_num in [7, 8, 9, 17, 35, 53]: # N, O, F, Cl, Br, I
- electron_feature += 0.8
- elif atomic_num == 6 and atom.GetIsAromatic():
- electron_feature += 0.6
- elif atomic_num == 6:
- electron_feature += 0.4
- elif atomic_num == 1:
- electron_feature += 0.2
- else:
- electron_feature += 0.5
- atom_count += 1
- hyperedge_features[i, 2] = electron_feature / max(1, atom_count)
- connectivity = 0.0
- for other_idx, other_edge in enumerate(self.hyperedges):
- if i != other_idx:
- if set(edge).intersection(set(other_edge)):
- connectivity += 1.0
- hyperedge_features[i, 3] = min(1.0, connectivity / max(1, len(self.hyperedges) / 2))
- pharmacophore_patterns = {
- 'HBondDonor': ['[OH]', '[NH]', '[NH2]'],
- 'HBondAcceptor': ['[O]', '[N;!$(N-*=O)]'],
- 'Hydrophobic': ['[C;!$(C=O);!$(C#N)]', '[c]'],
- 'Aromatic': ['c1ccccc1', 'c1ccncc1'],
- 'Charged': ['[+]', '[-]', '[N+]', '[O-]']
- }
- is_pharmacophore = False
- for pattern_list in pharmacophore_patterns.values():
- for pattern in pattern_list:
- patt = Chem.MolFromSmarts(pattern)
- if patt and self.mol.HasSubstructMatch(patt):
- matches = self.mol.GetSubstructMatches(patt)
- for match in matches:
- if any(atom_idx in edge for atom_idx in match):
- is_pharmacophore = True
- break
- if is_pharmacophore:
- break
- if is_pharmacophore:
- break
- hyperedge_features[i, 4] = 0.9 if is_pharmacophore else 0.4
- if hyperedge_features.shape[0] != num_hyperedges:
- print(
- f"Warning: Final hyperedge feature matrix shape {hyperedge_features.shape} doesn't match expected size {num_hyperedges}. Fixing.")
- corrected_features = np.zeros((num_hyperedges, 5), dtype=np.float32)
- min_size = min(hyperedge_features.shape[0], num_hyperedges)
- corrected_features[:min_size] = hyperedge_features[:min_size]
- hyperedge_features = corrected_features
- return hyperedge_features
- def get_adjacency_matrix(self):
- num_nodes = self.mol.GetNumAtoms()
- num_hyperedges = len(self.hyperedges)
- adjacency_matrix = np.zeros((num_nodes, num_hyperedges), dtype=int)
- for hyperedge_idx, hyperedge in enumerate(self.hyperedges):
- for node in hyperedge:
- adjacency_matrix[node][hyperedge_idx] = 1
- return adjacency_matrix
- class MoleculeData(Data):
- def __init__(self, x=None, edge_index=None, y=None, hyperedge_attr=None, smiles=None, **kwargs):
- super(MoleculeData, self).__init__(**kwargs)
- self.hyperedge_attr = None
- self.smiles = None
- if x is not None:
- if not isinstance(x, torch.Tensor):
- x = torch.FloatTensor(x)
- if x.size(0) == 0:
- print("Warning: Empty feature matrix provided")
- self.x = x
- if edge_index is not None:
- if not isinstance(edge_index, torch.Tensor):
- edge_index = torch.LongTensor(edge_index)
- if edge_index.dim() != 2 or edge_index.size(0) != 2:
- print(f"Warning: Unusual edge_index shape: {edge_index.shape}")
- self.edge_index = edge_index
- if y is not None:
- if not isinstance(y, torch.Tensor):
- y = torch.FloatTensor(y)
- self.y = y
- if hyperedge_attr is not None:
- if not isinstance(hyperedge_attr, torch.Tensor):
- hyperedge_attr = torch.FloatTensor(hyperedge_attr)
- self.hyperedge_attr = hyperedge_attr
- if smiles is not None:
- self.smiles = smiles
- @classmethod
- def from_cache(cls, filepath):
- data = torch.load(filepath)
- if not cls.verify_data(data):
- raise ValueError("Invalid cached data")
- return data
- @staticmethod
- def verify_data(data):
- if not isinstance(data, Data):
- print("Invalid data type")
- return False
- if not hasattr(data, 'x') or not hasattr(data, 'edge_index') or not hasattr(data, 'y'):
- print("Missing required attributes in data")
- return False
- if data.x is None:
- print("Data contains None x attribute")
- return False
- if data.edge_index is None:
- print("Data contains None edge_index attribute")
- return False
- if data.y is None:
- print("Data contains None y attribute")
- return False
- if data.x.size(0) == 0:
- print("Empty x in data")
- return False
- if data.edge_index.numel() == 0:
- print("Empty edge_index in data")
- return False
- if not torch.isfinite(data.x).all():
- print("Non-finite values in data.x")
- return False
- if not torch.isfinite(data.y).all():
- print("Non-finite values in data.y")
- return False
- if hasattr(data, 'hyperedge_attr') and data.hyperedge_attr is not None:
- if not torch.isfinite(data.hyperedge_attr).all():
- print("Non-finite values in data.hyperedge_attr")
- return False
- num_nodes = data.x.size(0)
- if data.edge_index.numel() > 0:
- if data.edge_index.size(0) != 2:
- print(f"Invalid edge_index shape: expected (2, N), got {data.edge_index.shape}")
- return False
- node_indices = data.edge_index[0]
- edge_indices = data.edge_index[1]
- if node_indices.min().item() < 0 or node_indices.max().item() >= num_nodes:
- print(
- f"Invalid node indices in edge_index: range [{node_indices.min().item()}, {node_indices.max().item()}], num_nodes={num_nodes}")
- return False
- if edge_indices.min().item() < 0:
- print(f"Invalid edge indices in edge_index: min={edge_indices.min().item()} is negative")
- return False
- if hasattr(data, 'hyperedge_attr') and data.hyperedge_attr is not None:
- num_hyperedges = data.hyperedge_attr.size(0)
- if edge_indices.max().item() >= num_hyperedges:
- print(
- f"Invalid edge indices in edge_index: max={edge_indices.max().item()}, num_hyperedges={num_hyperedges}")
- return False
- return True
- def get_hyperedge_attr(self):
- return self.hyperedge_attr
- class MoleculeDataset(Dataset):
- def __init__(self, data_path, dataset_name, task_type='classification',
- max_samples=None, feature_type='combined'):
- super(MoleculeDataset, self).__init__()
- self.feature_type = feature_type
- self.dataset_name = dataset_name.lower()
- self.task_type = task_type
- self.cache_dir = f"./processed_data/{dataset_name}/"
- self.metadata_file = os.path.join(self.cache_dir, "metadata.pt")
- self.is_multi_label = False
- os.makedirs(self.cache_dir, exist_ok=True)
- self.label_encoding = {
- 'positive': 1,
- 'negative': -1,
- 'missing': 0,
- 'original_to_encoded': {},
- 'encoded_to_original': {}
- }
- if self._try_load_cache():
- print("\nLoaded processed data from cache")
- return
- print(f"\nLoading dataset from {data_path}")
- df = pd.read_csv(data_path)
- print(f"Total records in CSV: {len(df)}")
- df.columns = df.columns.str.strip().str.lower()
- dataset_handler = getattr(self, f"_process_{self.dataset_name}", None)
- if dataset_handler:
- dataset_handler(df)
- else:
- raise ValueError(f"Unsupported dataset: {dataset_name}")
- if max_samples is not None:
- print(f"\nUsing only the first {max_samples} samples for testing.")
- self.smiles_list = self.smiles_list[:max_samples]
- self.labels = self.labels[:max_samples]
- self.valid_smiles = []
- self.valid_indices = []
- self.processed_data = []
- self.hyperedge_attrs = {}
- print("\nProcessing molecules...")
- success_count = 0
- error_count = 0
- empty_count = 0
- for i, smiles in enumerate(tqdm(self.smiles_list)):
- if pd.isna(smiles):
- empty_count += 1
- continue
- try:
- mol = self._robust_smiles_to_mol(smiles)
- if mol is None:
- error_count += 1
- continue
- hypergraph = MoleculeHypergraph(feature_type=self.feature_type)
- try:
- hypergraph.build_from_mol(mol)
- except Exception as e:
- error_count += 1
- print(f"\nError building hypergraph for molecule {i} with SMILES {smiles}: {str(e)}")
- continue
- if hypergraph.node_features is None or hypergraph.hyperedge_index is None:
- error_count += 1
- print(f"\nInvalid hypergraph for molecule {i} with SMILES {smiles}")
- continue
- x = torch.FloatTensor(hypergraph.node_features)
- edge_index = torch.LongTensor(hypergraph.hyperedge_index)
- num_nodes = x.size(0)
- if edge_index.shape[0] != 2:
- error_count += 1
- print(
- f"\nInvalid edge_index shape for molecule {i} with SMILES {smiles}: expected (2, N), got {edge_index.shape}")
- continue
- node_indices = edge_index[0]
- edge_indices = edge_index[1]
- if node_indices.max() >= num_nodes or node_indices.min() < 0:
- error_count += 1
- print(
- f"\nInvalid node indices in edge_index for molecule {i} with SMILES {smiles}: range [{node_indices.min()}, {node_indices.max()}], num_nodes={num_nodes}")
- continue
- num_hyperedges = len(hypergraph.hyperedges)
- if edge_indices.max() >= num_hyperedges or edge_indices.min() < 0:
- error_count += 1
- print(
- f"\nInvalid edge indices in edge_index for molecule {i} with SMILES {smiles}: range [{edge_indices.min()}, {edge_indices.max()}], num_hyperedges={num_hyperedges}")
- continue
- y = self.process_label(i)
- if not (torch.isfinite(x).all() and torch.isfinite(y).all()):
- error_count += 1
- print(f"\nNon-finite values in molecule {i}")
- continue
- hyperedge_attr = hypergraph.generate_enhanced_hyperedge_attributes()
- if hyperedge_attr is None:
- print(f"Warning: No hyperedges found for molecule {i}. Creating default attribute.")
- hyperedge_attr = np.zeros((1, 5), dtype=np.float32)
- if isinstance(hyperedge_attr, torch.Tensor):
- self.hyperedge_attrs[smiles] = hyperedge_attr.cpu().numpy()
- else:
- self.hyperedge_attrs[smiles] = hyperedge_attr
- hyperedge_attr_tensor = torch.FloatTensor(hyperedge_attr)
- data = MoleculeData(
- x=x,
- edge_index=edge_index,
- y=y,
- hyperedge_attr=hyperedge_attr_tensor,
- smiles=smiles
- )
- data.smiles = smiles
- self.processed_data.append(data)
- self.valid_smiles.append(smiles)
- self.valid_indices.append(i)
- success_count += 1
- except Exception as e:
- error_count += 1
- print(f"\nError processing molecule {i}: {str(e)}")
- continue
- if len(self.processed_data) == 0:
- raise ValueError("No valid molecules processed!")
- self._check_data_validity()
- self._save_cache()
- print("\nProcessing Summary:")
- print(f"Total molecules: {len(self.smiles_list)}")
- print(f"Successfully processed: {success_count}")
- print(f"Failed to process: {error_count}")
- print(f"Empty SMILES: {empty_count}")
- self._print_label_statistics()
- def _robust_smiles_to_mol(self, smiles):
- mol = Chem.MolFromSmiles(smiles)
- if mol is not None:
- return mol
- mol = Chem.MolFromSmiles(smiles, sanitize=False)
- if mol is None:
- return None
- try:
- mol.UpdatePropertyCache(strict=False)
- for atom in mol.GetAtoms():
- if atom.GetSymbol() == 'N':
- if atom.GetExplicitValence() >= 4 and atom.GetFormalCharge() == 0:
- atom.SetFormalCharge(1)
- Chem.SanitizeMol(mol)
- return mol
- except Exception:
- try:
- Chem.SanitizeMol(mol, sanitizeOps=Chem.SanitizeFlags.SANITIZE_ALL ^ Chem.SanitizeFlags.SANITIZE_PROPERTIES)
- return mol
- except:
- return None
- def _process_bace(self, df):
- """处理 BACE 数据集(单标签分类)"""
- required_cols = ['mol', 'class']
- if not all(col in df.columns for col in required_cols):
- raise ValueError(f"bace dataset must contain columns: {required_cols}")
- self.smiles_list = df['mol'].values
- self.labels = df['class'].values
- self.labels = np.where(self.labels == 0, -1, self.labels)
- self.label_cols = ['class']
- self.is_multi_label = False
- self.process_label = lambda idx: torch.FloatTensor([[float(self.labels[idx])]])
- print("\nLabel distribution in original data:")
- print(df['class'].value_counts(normalize=True))
- def _process_bbbp(self, df):
- """处理 BBBP 数据集(单标签分类)"""
- required_cols = ['smiles', 'p_np']
- if not all(col in df.columns for col in required_cols):
- raise ValueError(f"BBBP dataset must contain columns: {required_cols}")
- self.smiles_list = df['smiles'].values
- self.labels = df['p_np'].values
- self.labels = np.where(self.labels == 0, -1, self.labels)
- self.label_cols = ['p_np']
- self.is_multi_label = False
- self.process_label = lambda idx: torch.FloatTensor([[float(self.labels[idx])]])
- print("\nLabel distribution in original data:")
- print(df['p_np'].value_counts(normalize=True))
- def _process_sider(self, df):
- """处理 SIDER 数据集(多标签分类)"""
- if 'smiles' not in df.columns:
- raise ValueError("SIDER dataset must contain a 'smiles' column")
- self.smiles_list = df['smiles'].values
- label_cols = [col for col in df.columns if col != 'smiles']
- self.label_cols = label_cols
- self.labels = df[label_cols].values.astype(float)
- self.labels = np.where(self.labels == 0, -1, self.labels)
- self.is_multi_label = True
- self.process_label = lambda idx: torch.FloatTensor(self.labels[idx]).unsqueeze(0)
- print("\nLabel distribution in original data:")
- print(df[label_cols].apply(lambda x: pd.value_counts(x, normalize=True)))
- def _process_clintox(self, df):
- """处理 ClinTox 数据集(单标签分类)"""
- if 'smiles' not in df.columns:
- raise ValueError("ClinTox dataset must contain a 'smiles' column")
- label_cols = ['fda_approved', 'ct_tox']
- required_cols = ['smiles'] + label_cols
- if not all(col in df.columns for col in required_cols):
- raise ValueError(f"ClinTox dataset must contain columns: {required_cols}")
- self.smiles_list = df['smiles'].values
- self.labels = df[label_cols].values.astype(float)
- self.labels = np.where(self.labels == 0, -1, self.labels)
- self.labels = np.nan_to_num(self.labels, nan=0)
- self.label_cols = label_cols
- self.is_multi_label = True
- self.process_label = lambda idx: torch.FloatTensor(self.labels[idx]).unsqueeze(0)
- def _process_tox21(self, df):
- """处理 Tox21 数据集(多标签分类)"""
- if 'smiles' not in df.columns:
- raise ValueError("Tox21 dataset must contain a 'smiles' column")
- self.smiles_list = df['smiles'].values
- label_cols = [col for col in df.columns if col.lower() not in ['smiles', 'mol_id']]
- self.label_cols = label_cols
- self.labels = df[label_cols].values.astype(float)
- self.labels = np.where(self.labels == 0, -1, self.labels)
- self.labels = np.nan_to_num(self.labels, nan=0)
- self.is_multi_label = True
- self.process_label = lambda idx: torch.FloatTensor(self.labels[idx]).unsqueeze(0)
- print("\nLabel distribution after processing:")
- print(df[label_cols].apply(lambda x: pd.value_counts(x, normalize=True)))
- def _process_toxcast(self, df):
- """处理 ToxCast 数据集(多标签分类)"""
- if 'smiles' not in df.columns:
- raise ValueError("ToxCast dataset must contain a 'smiles' column")
- self.smiles_list = df['smiles'].values
- label_cols = [col for col in df.columns if col != 'smiles']
- self.label_cols = label_cols
- self.labels = df[label_cols].values.astype(float)
- self.labels = np.where(self.labels == 0, -1, self.labels)
- self.labels = np.nan_to_num(self.labels, nan=0)
- self.is_multi_label = True
- self.process_label = lambda idx: torch.FloatTensor(self.labels[idx]).unsqueeze(0)
- print("\nLabel distribution in original data:")
- print(df[label_cols].apply(lambda x: pd.value_counts(x, normalize=True)))
- def _process_esol(self, df):
- """处理 ESOL 数据集(单目标回归)"""
- required_cols = ['smiles', 'measured log solubility in mols per litre']
- if not all(col in df.columns for col in required_cols):
- raise ValueError(f"ESOL dataset must contain columns: {required_cols}")
- self.smiles_list = df['smiles'].values
- self.labels = df['measured log solubility in mols per litre'].values.astype(float)
- self.label_cols = ['log_solubility']
- self.is_multi_label = False
- self.task_type = 'regression'
- self.process_label = lambda idx: torch.FloatTensor([[float(self.labels[idx])]])
- print("\nESOL dataset statistics:")
- print(f"Min solubility: {np.min(self.labels):.2f}")
- print(f"Max solubility: {np.max(self.labels):.2f}")
- print(f"Mean solubility: {np.mean(self.labels):.2f}")
- print(f"Std solubility: {np.std(self.labels):.2f}")
- def _process_freesolv(self, df):
- """处理 FreeSolv 数据集(单目标回归)"""
- required_cols = ['smiles', 'expt']
- if not all(col in df.columns for col in required_cols):
- raise ValueError(f"FreeSolv dataset must contain columns: {required_cols}")
- self.smiles_list = df['smiles'].values
- self.labels = df['expt'].values.astype(float)
- self.label_cols = ['expt']
- self.is_multi_label = False
- self.task_type = 'regression'
- self.process_label = lambda idx: torch.FloatTensor([[float(self.labels[idx])]])
- print("\nFreeSolv dataset statistics:")
- print(f"Min expt: {np.min(self.labels):.2f}")
- print(f"Max expt: {np.max(self.labels):.2f}")
- print(f"Mean expt: {np.mean(self.labels):.2f}")
- print(f"Std expt: {np.std(self.labels):.2f}")
- def _process_lipophilicity(self, df):
- """处理 Lipophilicity 数据集(单目标回归)"""
- required_cols = ['cmpd_chemblid', 'exp', 'smiles']
- if not all(col in df.columns for col in required_cols):
- raise ValueError(f"Lipophilicity dataset must contain columns: {required_cols}")
- self.smiles_list = df['smiles'].values
- self.labels = df['exp'].values.astype(float)
- self.label_cols = ['exp']
- self.is_multi_label = False
- self.task_type = 'regression'
- self.process_label = lambda idx: torch.FloatTensor([[float(self.labels[idx])]])
- print("\nLipophilicity dataset statistics:")
- print(f"Min exp: {np.min(self.labels):.2f}")
- print(f"Max exp: {np.max(self.labels):.2f}")
- print(f"Mean exp: {np.mean(self.labels):.2f}")
- print(f"Std exp: {np.std(self.labels):.2f}")
- def _process_qm8(self, df):
- """处理 QM8 数据集(多目标回归)"""
- required_cols = ['smiles']
- if not all(col in df.columns for col in required_cols):
- raise ValueError("QM8 dataset must contain a 'smiles' column")
- self.smiles_list = df['smiles'].values
- label_cols = [col for col in df.columns if col != 'smiles']
- self.label_cols = label_cols
- self.labels = df[label_cols].values.astype(float)
- self.is_multi_label = True
- self.task_type = 'regression'
- self.process_label = lambda idx: torch.FloatTensor(self.labels[idx]).unsqueeze(0)
- print("\nQM8 dataset statistics:")
- print(f"Label columns: {label_cols}")
- print(f"Min values: {np.min(self.labels, axis=0)}")
- print(f"Max values: {np.max(self.labels, axis=0)}")
- print(f"Mean values: {np.mean(self.labels, axis=0)}")
- print(f"Std values: {np.std(self.labels, axis=0)}")
- def _process_qm7(self, df):
- """
- 处理 QM7 数据集
- """
- if 'smiles' not in df.columns:
- raise ValueError("QM7 dataset must contain a 'smiles' column")
- possible_targets = ['u0_atom', 'u0', 'target', 'eat']
- target_col = next((c for c in possible_targets if c in df.columns), None)
- if target_col is None:
- target_col = [c for c in df.columns if c != 'smiles'][-1]
- print(f"Warning: Specific QM7 target column not found. Using '{target_col}' as target.")
- self.smiles_list = df['smiles'].values
- self.labels = df[target_col].values.astype(float)
- self.label_cols = [target_col]
- self.is_multi_label = False
- self.task_type = 'regression'
- self.process_label = lambda idx: torch.FloatTensor([[float(self.labels[idx])]])
- print(f"\nQM7 dataset statistics ({target_col}):")
- print(f"Total samples: {len(self.labels)}")
- print(f"Min: {np.min(self.labels):.4f}")
- print(f"Max: {np.max(self.labels):.4f}")
- print(f"Mean: {np.mean(self.labels):.4f}")
- print(f"Std: {np.std(self.labels):.4f}")
- def _check_data_validity(self):
- if len(self.processed_data) == 0:
- print("Warning: No processed data to check validity.")
- return
- if self.task_type == 'classification':
- invalid_tasks = []
- if self.is_multi_label:
- if not hasattr(self, 'label_cols') or not self.label_cols:
- print("Warning: Cannot check validity, label_cols not defined.")
- return
- num_tasks = len(self.label_cols)
- if num_tasks == 0:
- print("Warning: Cannot check validity, num_tasks is zero.")
- return
- for task_idx in range(num_tasks):
- task_labels = []
- for data in self.processed_data:
- if hasattr(data, 'y') and data.y is not None and data.y.dim() == 2 and data.y.shape[0] == 1 and data.y.shape[1] > task_idx:
- try:
- label_value = data.y[0, task_idx].item()
- task_labels.append(label_value)
- except IndexError:
- print(f"Warning: IndexError accessing data.y[0, {task_idx}] for data with y shape {data.y.shape}. Skipping.")
- except Exception as e:
- print(f"Warning: Error accessing label for task {task_idx}: {e}. Skipping.")
- if not task_labels:
- print(f"Warning: No valid labels found for task {task_idx} ({self.label_cols[task_idx]}).")
- invalid_tasks.append((self.label_cols[task_idx], []))
- continue
- valid_labels = [v for v in task_labels if not np.isnan(v)]
- if not valid_labels:
- print(f"Warning: All labels are NaN for task {task_idx} ({self.label_cols[task_idx]}) after filtering.")
- invalid_tasks.append((self.label_cols[task_idx], []))
- continue
- unique = np.unique(valid_labels)
- if len(unique) < 2:
- invalid_tasks.append((self.label_cols[task_idx], unique))
- else:
- try:
- label_values = []
- for data in self.processed_data:
- if hasattr(data, 'y') and data.y is not None and data.y.dim() == 2 and data.y.shape == (1, 1):
- try:
- label_value = data.y[0, 0].item()
- label_values.append(label_value)
- except IndexError:
- print(f"Warning: IndexError accessing data.y[0, 0] for data with y shape {data.y.shape}. Skipping.")
- except Exception as e:
- print(f"Warning: Error accessing single label: {e}. Skipping.")
- if not label_values:
- print("Warning: No valid labels found for single-label task.")
- invalid_tasks.append(('main_label', []))
- else:
- unique = np.unique(label_values)
- if len(unique) < 2:
- invalid_tasks.append(('main_label', unique))
- except Exception as e:
- print(f"Error analyzing single-label distribution: {str(e)}")
- if invalid_tasks:
- print("\nWARNING: Found tasks with only one class (or no valid labels):")
- for task, classes in invalid_tasks:
- if classes:
- print(f" Task {task}: classes {classes}")
- else:
- print(f" Task {task}: No valid labels found.")
- print("This may cause evaluation metrics like AUC/AUPR to fail or be unreliable!")
- def get_original_label_value(self, encoded_value):
- if hasattr(self, 'label_encoding') and 'encoded_to_original' in self.label_encoding:
- return self.label_encoding['encoded_to_original'].get(encoded_value, encoded_value)
- return encoded_value
- def is_missing_label(self, encoded_value):
- if hasattr(self, 'label_encoding'):
- return encoded_value == self.label_encoding['missing']
- return False
- def _print_label_statistics(self):
- try:
- if self.is_multi_label and self.dataset_name in ['sider', 'tox21', 'toxcast']:
- print("\nLabel distribution in processed data:")
- for idx, col in enumerate(self.label_cols):
- if idx < self.labels.shape[1]:
- label_values = self.labels[:, idx]
- unique, counts = np.unique(label_values, return_counts=True)
- print(f"{col}:")
- for u, c in zip(unique, counts):
- print(f" Label {u}: {c} samples ({c / len(label_values) * 100:.2f}%)")
- elif self.dataset_name in ['qm8', 'qm9']:
- try:
- processed_labels_array = []
- for data in self.processed_data:
- if data.y.numel() > 0:
- processed_labels_array.append(data.y.cpu().numpy())
- processed_labels_array = np.array(processed_labels_array)
- print("\nProcessed label statistics (per dimension):")
- for i, col in enumerate(self.label_cols):
- if i < processed_labels_array.shape[1]:
- col_values = processed_labels_array[:, i]
- print(
- f"{col}: Min={col_values.min():.4f}, Max={col_values.max():.4f}, Mean={col_values.mean():.4f}, Std={col_values.std():.4f}")
- except Exception as e:
- print(f"Error computing label statistics: {str(e)}")
- else:
- try:
- processed_labels = []
- for data in self.processed_data:
- if data.y.numel() >= 1:
- processed_labels.append(data.y[0].item())
- unique_labels = np.unique(processed_labels)
- print("\nLabel distribution in processed data:")
- for label in unique_labels:
- count = sum(1 for y in processed_labels if y == label)
- print(f"Label {label}: {count} samples ({count / len(processed_labels) * 100:.2f}%)")
- except Exception as e:
- print(f"Error computing label distribution: {str(e)}")
- except Exception as e:
- print(f"Error in label statistics calculation: {str(e)}")
- def _save_hyperedge_attrs(self):
- for data in self.processed_data:
- if hasattr(data, 'smiles') and data.smiles and hasattr(data,
- 'hyperedge_attr') and data.hyperedge_attr is not None:
- if isinstance(data.hyperedge_attr, torch.Tensor):
- self.hyperedge_attrs[data.smiles] = data.hyperedge_attr.cpu().numpy()
- else:
- self.hyperedge_attrs[data.smiles] = data.hyperedge_attr
- hyperedge_attrs_file = os.path.join(self.cache_dir, "hyperedge_attrs.pt")
- torch.save(self.hyperedge_attrs, hyperedge_attrs_file)
- print(f"Saved hyperedge attributes for {len(self.hyperedge_attrs)} molecules")
- def _get_cache_filename(self, idx):
- return os.path.join(self.cache_dir, f"mol_{idx}.pt")
- def _save_metadata(self):
- has_hyperedge_attr = False
- if self.processed_data and len(self.processed_data) > 0:
- sample_item = self.processed_data[0]
- has_hyperedge_attr = hasattr(sample_item, 'hyperedge_attr') and sample_item.hyperedge_attr is not None
- metadata = {
- 'dataset_name': self.dataset_name,
- 'task_type': self.task_type,
- 'num_classes': self._get_num_classes(),
- 'label_cols': self.label_cols,
- 'is_multi_label': self.is_multi_label,
- 'valid_indices': self.valid_indices,
- 'has_hyperedge_attr': has_hyperedge_attr,
- 'label_encoding': getattr(self, 'label_encoding', {})
- }
- torch.save(metadata, self.metadata_file)
- def _load_metadata(self):
- if os.path.exists(self.metadata_file):
- try:
- metadata = torch.load(self.metadata_file)
- if metadata.get('dataset_name') == self.dataset_name:
- self.label_cols = metadata.get('label_cols', [])
- self.is_multi_label = metadata.get('is_multi_label', False)
- self.valid_indices = metadata.get('valid_indices', [])
- self.task_type = metadata.get('task_type', self.task_type)
- self.has_hyperedge_attr = metadata.get('has_hyperedge_attr', False)
- self.label_encoding = metadata.get('label_encoding', {
- 'positive': 1,
- 'negative': -1,
- 'missing': 0,
- 'original_to_encoded': {},
- 'encoded_to_original': {}
- })
- return True
- return False
- except Exception as e:
- print(f"Error loading metadata: {str(e)}")
- return False
- return False
- def _try_load_cache(self):
- if not self._load_metadata():
- return False
- cache_files = [f for f in os.listdir(self.cache_dir) if
- f.endswith(".pt") and not f == "metadata.pt" and not f == "hyperedge_attrs.pt"]
- if not cache_files:
- return False
- print("\nChecking cached data...")
- self.hyperedge_attrs = {}
- hyperedge_attrs_file = os.path.join(self.cache_dir, "hyperedge_attrs.pt")
- if os.path.exists(hyperedge_attrs_file):
- try:
- self.hyperedge_attrs = torch.load(hyperedge_attrs_file)
- print(f"Loaded hyperedge attributes dictionary for {len(self.hyperedge_attrs)} molecules")
- except Exception as e:
- print(f"Error loading hyperedge attributes: {e}")
- self.hyperedge_attrs = {}
- self.processed_data = []
- self.valid_smiles = []
- success = 0
- failures = 0
- max_idx = -1
- for f in cache_files:
- try:
- idx = int(f.split('_')[1].split('.')[0])
- max_idx = max(max_idx, idx)
- except:
- continue
- for idx in tqdm(range(max_idx + 1)):
- cache_file = self._get_cache_filename(idx)
- if os.path.exists(cache_file):
- try:
- data = torch.load(cache_file)
- if not hasattr(data, 'x') or data.x is None or data.x.shape[0] == 0:
- print(f"Skipping invalid cache file (bad x): {cache_file}")
- failures += 1
- continue
- if not hasattr(data, 'edge_index') or data.edge_index is None:
- print(f"Skipping invalid cache file (bad edge_index): {cache_file}")
- failures += 1
- continue
- if not hasattr(data, 'y') or data.y is None:
- print(f"Skipping invalid cache file (bad y): {cache_file}")
- failures += 1
- continue
- smiles = getattr(data, 'smiles', None)
- hyperedge_attr = getattr(data, 'hyperedge_attr', None)
- if hyperedge_attr is None and smiles is not None and smiles in self.hyperedge_attrs:
- hyperedge_attr_np = self.hyperedge_attrs[smiles]
- hyperedge_attr = torch.FloatTensor(hyperedge_attr_np)
- print(f"Recovered hyperedge attributes for molecule with SMILES {smiles}")
- cleaned_data = MoleculeData(
- x=data.x.clone(),
- edge_index=data.edge_index.clone(),
- y=data.y.clone(),
- hyperedge_attr=hyperedge_attr.clone() if hyperedge_attr is not None else None,
- smiles=smiles
- )
- self.processed_data.append(cleaned_data)
- if smiles is not None:
- self.valid_smiles.append(smiles)
- success += 1
- except Exception as e:
- print(f"Error loading cache file {cache_file}: {str(e)}")
- failures += 1
- print(f"Loaded {success}/{len(cache_files)} valid cached entries ({failures} failures)")
- return success > 0
- def _save_cache(self):
- print("\nSaving processed data to cache...")
- os.makedirs(self.cache_dir, exist_ok=True)
- for idx, data in enumerate(tqdm(self.processed_data)):
- try:
- smiles = getattr(data, 'smiles', None)
- hyperedge_attr = getattr(data, 'hyperedge_attr', None)
- data_cpu = MoleculeData(
- x=data.x.cpu(),
- edge_index=data.edge_index.cpu(),
- y=data.y.cpu(),
- hyperedge_attr=hyperedge_attr.cpu() if hyperedge_attr is not None else None,
- smiles=smiles
- )
- torch.save(data_cpu, self._get_cache_filename(idx))
- except Exception as e:
- print(f"Error saving {idx}: {str(e)}")
- self._save_hyperedge_attrs()
- self._save_metadata()
- print(f"Saved metadata to {self.metadata_file}")
- def len(self):
- return len(self.processed_data)
- def get(self, idx):
- return self.processed_data[idx]
- @property
- def num_node_features(self):
- if len(self.processed_data) == 0:
- raise ValueError("Dataset is empty!")
- return self.processed_data[0].x.size(1)
- def _get_num_classes(self):
- if self.task_type == 'regression':
- if self.is_multi_label:
- return len(self.label_cols)
- else:
- return 1
- elif self.is_multi_label:
- return len(self.label_cols)
- else:
- return 1
- @property
- def num_classes(self):
- if len(self.processed_data) == 0:
- raise ValueError("Dataset is empty!")
- return self._get_num_classes()
dataset.py at commit 171920e, no license · at the source
Overview
- School of Information and Control Engineering, China University of Mining and Technology, Xuzhou 221116, P. R. China
- National Key Laboratory of Agricultural Microbiology, College of Veterinary Medicine, Huazhong Agricultural University, 430070 Wuhan, Hubei, P. R. China
- Faculty of Science, Melbourne Veterinary School, The University of Melbourne, Melbourne, Victoria, Australia
Abstract
Accurate molecular property prediction is a central task in drug discovery, yet existing methods often struggle to simultaneously capture higher-order molecular structures and chemically grounded semantic information. Graph neural networks are limited to pairwise atomic interactions, while Simplified Molecular Input Line Entry System-based language models lack explicit structural grounding, leading to incomplete structure–property representations. In this work, we propose HYG-mol, an interpretable molecular property prediction framework that integrates hypergraph-based structural modeling with multimodal chemical semantics. Molecules are represented as hypergraphs in which chemically meaningful substructures, such as functional groups and ring systems, are explicitly encoded as hyperedges, enabling direct modeling of higher-order structural dependencies. Chemical semantic information derived from a pretrained language model is fused with physicochemical descriptors at the atomic level. A hypergraph attention network is employed to capture cross-scale interactions and to identify substructures relevant to the prediction task. Extensive evaluations on MoleculeNet benchmark datasets demonstrate that HYG-mol consistently outperforms state-of-the-art baseline methods across both classification and regression tasks. Ablation and interpretability analyses further validate the effectiveness of the proposed representation and reveal strong correspondence between model-identified substructures and chemically meaningful motifs. Overall, HYG-mol provides a unified and interpretable framework for molecular property prediction by explicitly grounding chemical semantics in higher-order structural representations.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 11 matches between paragraphs and lines of code.
sutera777/HYG-mol
171920ec65c262afd5beaf00af3eb67f706e60b8, 15 March 2026Availability: 1 check, the latest on 29 September 2026: the link answers
- 29 September 2026: the link answers
16 files
- __init__.py, Python, 1 line
- src/
__init__.py , Python, 1 line - src/
config.py , Python, 46 lines - src/
data/ , Python, 1 line__init__.py - src/
data/ , Python, 210 linesdataloader.py - src/
data/ , Python, 1,787 lines, 7 matchesdataset.py - src/
explainability/ , Python, 1 line__init__.py - src/
explainability/ , Python, 851 linesanalyzer.py - src/
main.py , Python, 487 lines, 2 matches - src/
models/ , Python, 1 line__init__.py - src/
models/ , Python, 321 linesnetworks.py - src/
training/ , Python, 1 line__init__.py - src/
training/ , Python, 389 lines, 1 matchtrainer.py - src/
utils/ , Python, 1 line__init__.py - src/
utils/ , Python, 381 lines, 1 matchutils.py - README.md, Text, 56 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 15 scripts, each with its path and the digest of its content;
- 11 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data Availability
The data used to support the findings of this study are available from the corresponding authors upon reasonable request. The molecular property data were derived from the MoleculeNet benchmark dataset (https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 29 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 1 funder, 26 references.
Cite
This paper
Ma, J., Yang, Q., Zhang, L., Liu, H., & Zheng, Y. (2026). HYG-mol: An Interpretable Multimodal Hypergraph Framework for Molecular Property Prediction. Computational and structural biotechnology journal, 35(1), 0036. https://
BibTeX
@article{ma2026hyg,
author = {Ma, Jiani and Yang, Qi and Zhang, Lin and Liu, Hui and Zheng, Yuanting},
title = {{HYG-mol: An Interpretable Multimodal Hypergraph Framework for Molecular Property Prediction}},
journal = {Computational and structural biotechnology journal},
year = {2026},
month = apr,
volume = {35},
number = {1},
pages = {0036},
publisher = {AAAS Science Partner Journal Program},
issn = {2001-0370},
doi = {10.34133/
url = {https://
pmid = {41971950},
pmcid = {PMC13062488}
}
RIS
TY - JOUR
AU - Ma, Jiani
AU - Yang, Qi
AU - Zhang, Lin
AU - Liu, Hui
AU - Zheng, Yuanting
TI - HYG-mol: An Interpretable Multimodal Hypergraph Framework for Molecular Property Prediction
T2 - Computational and structural biotechnology journal
J2 - Comput Struct Biotechnol J
PY - 2026
DA - 2026/
VL - 35
IS - 1
SP - 0036
SN - 2001-0370
PB - AAAS Science Partner Journal Program
DO - 10.34133/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.34133/
"type": "article-journal",
"title": "HYG-mol: An Interpretable Multimodal Hypergraph Framework for Molecular Property Prediction",
"container-title": "Computational and structural biotechnology journal",
"author": [
{
"family": "Ma",
"given": "Jiani"
},
{
"family": "Yang",
"given": "Qi"
},
{
"family": "Zhang",
"given": "Lin"
},
{
"family": "Liu",
"given": "Hui"
},
{
"family": "Zheng",
"given": "Yuanting"
}
],
"container-title-short":
"volume": "35",
"issue": "1",
"page": "0036",
"DOI": "10.34133/
"PMID": "41971950",
"PMCID": "PMC13062488",
"ISSN": "2001-0370",
"publisher": "AAAS Science Partner Journal Program",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
9
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.34133/csbj.0184 [code]
- Cross-Species Multitask Learning with Molecular and ADME Descriptors for Liver Microsomal Metabolic Stability.Journal: Computational and structural biotechnology journalIn common: RDKit, PyTorch Geometric, PyTorch, 4 other tools, cellular / molecular, 3 references
- [2] doi:10.1002/pro.70695 [code]
- MIF-MAPMS: Enhancing identification of myelin autoantigenic peptides in multiple sclerosis through multimodal information fusion.Journal: Protein science : a publication of the Protein SocietyIn common: RDKit, PyTorch, scikit-learn, 3 other tools, cellular / molecular, 2 references
- [3] doi:10.3390/molecules31111972 [code]
- A Bond-Level Sequence Framework for Molecular Representation Learning with Structural Constraints.Journal: Molecules (Basel, Switzerland)In common: RDKit, PyTorch, scikit-learn, 2 other tools, cellular / molecular, 2 references
- [4] doi:10.3390/ijms27156614 [code]
- Candidalysin Inhibits &
lt;i& gt;Porphyromonas gingivalis& lt;/ i& gt; Lipoprotein-Induced IL-1β Production in BV-2 Microglia via Hydrophobic Microbial Interactions. Journal: International journal of molecular sciencesIn common: RDKit, PyTorch Geometric, PyTorch, 4 other tools, cellular / molecular - [5] doi:10.1038/s41586-026-10670-w [code]
- Zero-shot design of drug-binding proteins via neural iterative selection-expansion.Journal: NatureIn common: RDKit, PyTorch Geometric, PyTorch, 4 other tools, cellular / molecular
- [6] doi:10.1038/s41598-026-53415-5 [code]
- Computational design and immunoinformatics validation of a T cell multi-epitope vaccine targeting glioblastoma stem cells.Journal: Scientific reportsIn common: RDKit, PyTorch Geometric, PyTorch, 4 other tools, cellular / molecular
- [7] doi:10.1038/s41586-026-10391-0 [code]
- Cell-type-targeted mitochondrial transplantation rescues cell degeneration.Journal: NatureIn common: RDKit, PyTorch Geometric, PyTorch, 4 other tools, cellular / molecular
- [8] doi:10.1038/s42003-026-10957-8 [code]
- Brain defence by the extracellular matrix protein Cochlin.Journal: Communications biologyIn common: RDKit, Hugging Face Transformers, PyTorch, 4 other tools, cellular / molecular
- [9] doi:10.1039/d6ra03343a [code]
- A benchmark dataset and interpretable deep learning framework for drug-induced developmental neurotoxicity prediction.Journal: RSC advancesIn common: RDKit, scikit-learn, pandas, 2 other tools, 2 references
- [10] doi:10.1371/journal.pone.0345854 [code]
- Shedding light on neural learning to rank models for anticancer drug prioritization.Journal: PloS oneIn common: RDKit, PyTorch Geometric, PyTorch, 4 other tools
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 15 scripts, and 11 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:a93f4e92b0094f2a…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
