Spaces:
Sleeping
Sleeping
| from typing import Any | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| from rdkit import Chem | |
| from torch.utils.data import Dataset | |
| from utils import smiles_to_graph | |
| # ───────────────────────────────────────────────────────────────── | |
| # Data Contract Exceptions & Loaders | |
| # ───────────────────────────────────────────────────────────────── | |
| class DataContractError(Exception): | |
| """Raised when dataset CSV files fail schema or integrity contracts.""" | |
| class GTExTissueLoader: | |
| """ | |
| Contract-conforming GTEx tissue profile loader. | |
| Validates data/tissue_profiles.csv against contract specifications. | |
| """ | |
| def __init__(self, profiles: dict[str, torch.Tensor]): | |
| self.profiles = profiles | |
| def from_csv(cls, csv_path: str) -> "GTExTissueLoader": | |
| df = pd.read_csv(csv_path) | |
| expected_organs = {"Liver", "Heart", "Brain", "Kidney", "Lung"} | |
| actual_organs = set(df["organ"].unique()) | |
| if not expected_organs.issubset(actual_organs): | |
| raise DataContractError( | |
| f"Missing required organs in {csv_path}. Expected at least {expected_organs}, got {actual_organs}" | |
| ) | |
| feat_cols = [c for c in df.columns if c.startswith("feature_")] | |
| if len(feat_cols) != 128: | |
| raise DataContractError( | |
| f"Expected 128 tissue feature columns in {csv_path}, found {len(feat_cols)}" | |
| ) | |
| if df[feat_cols].isna().any().any(): | |
| raise DataContractError(f"NaN values found in tissue features of {csv_path}") | |
| profiles = {} | |
| for _, row in df.iterrows(): | |
| organ = str(row["organ"]) | |
| vec = torch.tensor(row[feat_cols].values.astype(np.float32)) | |
| profiles[organ] = vec | |
| return cls(profiles) | |
| def __contains__(self, organ: str) -> bool: | |
| return organ in self.profiles | |
| def __getitem__(self, organ: str) -> torch.Tensor: | |
| if organ not in self.profiles: | |
| raise DataContractError(f"Organ '{organ}' not found in loaded GTEx tissue profiles.") | |
| return self.profiles[organ] | |
| def load_labeled_records(csv_path: str, tissue: GTExTissueLoader) -> pd.DataFrame: | |
| """ | |
| Contract-conforming labeled ADR record loader. | |
| Validates data/adr_records.csv against data contract constraints. | |
| """ | |
| df = pd.read_csv(csv_path) | |
| expected_header = [ | |
| "smiles", "organ", "Hepatotoxicity", "Arrhythmia", "Seizure", | |
| "Nephrotoxicity", "Pneumonitis", "Nausea", "Headache", "Dizziness", | |
| "Fatigue", "Rash" | |
| ] | |
| if list(df.columns) != expected_header: | |
| raise DataContractError( | |
| f"Header mismatch in {csv_path}.\nExpected: {expected_header}\nGot: {list(df.columns)}" | |
| ) | |
| if df.isna().any().any(): | |
| raise DataContractError(f"Missing/NaN values detected in {csv_path}") | |
| for idx, row in df.iterrows(): | |
| smiles = str(row["smiles"]) | |
| organ = str(row["organ"]) | |
| if organ not in tissue: | |
| raise DataContractError(f"Row {idx}: Organ '{organ}' not in GTEx tissue loader.") | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is None: | |
| raise DataContractError(f"Row {idx}: Invalid SMILES string '{smiles}'") | |
| for adr in expected_header[2:]: | |
| val = row[adr] | |
| if val not in (0, 1, 0.0, 1.0): | |
| raise DataContractError(f"Row {idx}: Non-binary target label for {adr}: {val}") | |
| return df | |
| # ───────────────────────────────────────────────────────────────── | |
| # Reproducibility seed | |
| # ───────────────────────────────────────────────────────────────── | |
| SEED = 42 | |
| np.random.seed(SEED) | |
| # ───────────────────────────────────────────────────────────────── | |
| # 10 Human Organs | |
| # ───────────────────────────────────────────────────────────────── | |
| ORGAN_NAMES = [ | |
| "Liver", "Heart", "Brain", "Kidney", "Lung", | |
| "Pancreas", "Spleen", "Intestine", "Skin", "Bone_Marrow", | |
| ] | |
| _TISSUE_ALPHA = { | |
| "Liver": 1.80, | |
| "Heart": 1.40, | |
| "Brain": 0.70, | |
| "Kidney": 1.20, | |
| "Lung": 1.00, | |
| "Pancreas": 0.90, | |
| "Spleen": 1.10, | |
| "Intestine": 1.50, | |
| "Skin": 0.85, | |
| "Bone_Marrow": 0.60, | |
| } | |
| TISSUE_DIM = 1024 | |
| GTEX_TISSUE_PROFILES: dict[str, torch.Tensor] = { | |
| organ: torch.tensor( | |
| np.random.dirichlet(np.ones(TISSUE_DIM) * alpha).astype(np.float32) | |
| ) | |
| for organ, alpha in _TISSUE_ALPHA.items() | |
| } | |
| MEDDRA_ADR_CLASSES = [ | |
| "Hepatotoxicity", # 0 | |
| "Cardiotoxicity", # 1 | |
| "Nephrotoxicity", # 2 | |
| "Neurotoxicity", # 3 | |
| "Pulmotoxicity", # 4 | |
| "Gastrointestinal Toxicity", # 5 | |
| "Hematotoxicity", # 6 | |
| "Dermatological Reaction", # 7 | |
| "Immunotoxicity", # 8 | |
| "Metabolic Disruption", # 9 | |
| ] | |
| _RAW_DRUG_DEFS = [ | |
| ("Acetaminophen", "CC(=O)NC1=CC=C(O)C=C1", ["Liver", "Kidney"]), | |
| ("Aspirin", "CC(=O)OC1=CC=CC=C1C(=O)O", ["Gastrointestinal Toxicity"]), | |
| ("Ibuprofen", "CC(C)CC1=CC=C(C=C1)C(C)C(=O)O", ["Kidney", "Gastrointestinal Toxicity"]), | |
| ("Diclofenac", "OC(=O)Cc1ccccc1Nc1c(Cl)cccc1Cl", ["Liver", "Kidney", "Gastrointestinal Toxicity"]), | |
| ("Naproxen", "COc1ccc2cc(C(C)C(=O)O)ccc2c1", ["Gastrointestinal Toxicity", "Kidney"]), | |
| ] | |
| BENCHMARK_DRUGS: list[dict[str, Any]] = [] | |
| for idx, (name, smiles, tox) in enumerate(_RAW_DRUG_DEFS): | |
| BENCHMARK_DRUGS.append({"name": name, "smiles": smiles, "toxic_organs": tox}) | |
| _ORGAN_TO_ADR_IDX = { | |
| "Liver": 0, | |
| "Heart": 1, | |
| "Kidney": 2, | |
| "Brain": 3, | |
| "Lung": 4, | |
| "Intestine": 5, | |
| "Pancreas": 9, | |
| "Spleen": 8, | |
| "Skin": 7, | |
| "Bone_Marrow": 6, | |
| } | |
| _SYSTEMIC_TO_ADR_IDX = { | |
| "Hepatotoxicity": 0, | |
| "Cardiotoxicity": 1, | |
| "Nephrotoxicity": 2, | |
| "Neurotoxicity": 3, | |
| "Pulmotoxicity": 4, | |
| "Gastrointestinal Toxicity": 5, | |
| "Hematotoxicity": 6, | |
| "Dermatological Reaction": 7, | |
| "Immunotoxicity": 8, | |
| "Metabolic Disruption": 9, | |
| } | |
| def _build_target_vector(organ_name: str, toxic_organs: list[str]) -> torch.Tensor: | |
| target = torch.zeros(10, dtype=torch.float32) | |
| organ_adr_idx = _ORGAN_TO_ADR_IDX.get(organ_name) | |
| if organ_adr_idx is not None and organ_name in toxic_organs: | |
| target[organ_adr_idx] = 1.0 | |
| for tox in toxic_organs: | |
| if tox in _SYSTEMIC_TO_ADR_IDX: | |
| target[_SYSTEMIC_TO_ADR_IDX[tox]] = 1.0 | |
| return target | |
| class EpiADRDataset(Dataset): | |
| def __init__( | |
| self, | |
| drugs: list[dict[str, Any]] | None = None, | |
| repeat: int = 4, | |
| ): | |
| self.drugs = drugs if drugs is not None else BENCHMARK_DRUGS | |
| self.repeat = repeat | |
| self.samples: list[dict[str, Any]] = [] | |
| self._build() | |
| def _build(self): | |
| graph_cache: dict[str, Any] = {} | |
| for item in self.drugs: | |
| smiles = item["smiles"] | |
| if smiles not in graph_cache: | |
| try: | |
| node_feats, edge_index, _ = smiles_to_graph(smiles) | |
| graph_cache[smiles] = (node_feats, edge_index) | |
| except Exception: | |
| graph_cache[smiles] = None | |
| for _ in range(self.repeat): | |
| for item in self.drugs: | |
| smiles = item["smiles"] | |
| drug_name = item.get("name", "Unknown") | |
| toxic_list = item.get("toxic_organs", []) | |
| cached = graph_cache.get(smiles) | |
| if cached is None: | |
| continue | |
| node_feats, edge_index = cached | |
| for organ_name in ORGAN_NAMES: | |
| tissue_vec = GTEX_TISSUE_PROFILES[organ_name] | |
| target = _build_target_vector(organ_name, toxic_list) | |
| self.samples.append({ | |
| "smiles": smiles, | |
| "drug_name": drug_name, | |
| "node_feats": node_feats, | |
| "edge_index": edge_index, | |
| "organ_name": organ_name, | |
| "tissue_vec": tissue_vec, | |
| "target": target, | |
| }) | |
| def __len__(self): | |
| return len(self.samples) | |
| def __getitem__(self, idx): | |
| return self.samples[idx] | |
| def custom_collate_fn(batch: list[dict[str, Any]]) -> dict[str, Any]: | |
| node_feats_list = [] | |
| edge_index_list = [] | |
| batch_index_list = [] | |
| tissue_vec_list = [] | |
| target_list = [] | |
| smiles_list = [] | |
| node_offset = 0 | |
| for graph_idx, sample in enumerate(batch): | |
| nf = sample["node_feats"] | |
| ei = sample["edge_index"] | |
| n = nf.shape[0] | |
| node_feats_list.append(nf) | |
| edge_index_list.append(ei + node_offset) | |
| batch_index_list.append(torch.full((n,), graph_idx, dtype=torch.long)) | |
| tissue_vec_list.append(sample["tissue_vec"]) | |
| target_list.append(sample["target"]) | |
| smiles_list.append(sample["smiles"]) | |
| node_offset += n | |
| return { | |
| "x": torch.cat(node_feats_list, dim=0), | |
| "edge_index": torch.cat(edge_index_list, dim=1), | |
| "batch": torch.cat(batch_index_list, dim=0), | |
| "tissue_vec": torch.stack(tissue_vec_list, dim=0), | |
| "y": torch.stack(target_list, dim=0), | |
| "smiles": smiles_list, | |
| } | |