candle-fire / extraction /normalizer.py
KevinIsInCoding
feat: automate ALS seed entities from trials and paper corpus
e53bbe5
Raw
History Blame Contribute Delete
6.89 kB
"""
Entity name normalization to canonical IDs.
Priority: static alias table β†’ known compound map β†’ slugify fallback.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from config import CANONICAL_IDS_PATH
# ── Gene / protein alias table ────────────────────────────────────────────────
_GENE_ALIASES: dict[str, str] = {
# TDP-43 / TARDBP
"TDP-43": "TARDBP", "TDP43": "TARDBP", "tdp-43": "TARDBP", "tdp43": "TARDBP",
"TAR DNA-binding protein 43": "TARDBP",
"TAR DNA binding protein 43": "TARDBP",
"TAR DNA-binding protein": "TARDBP",
# FUS
"FUS/TLS": "FUS", "TLS": "FUS", "TLS/FUS": "FUS", "fus": "FUS",
"fused in sarcoma": "FUS",
# C9orf72
"C9ORF72": "C9orf72", "c9orf72": "C9orf72", "C9": "C9orf72",
"chromosome 9 open reading frame 72": "C9orf72",
# SOD1
"SOD-1": "SOD1", "sod1": "SOD1",
"superoxide dismutase 1": "SOD1",
"Cu/Zn-superoxide dismutase": "SOD1",
"copper-zinc superoxide dismutase": "SOD1",
# ATXN2
"ataxin-2": "ATXN2", "ataxin 2": "ATXN2", "SCA2": "ATXN2",
# Optineurin
"optineurin": "OPTN",
# Ubiquilin
"ubiquilin-2": "UBQLN2", "ubiquilin2": "UBQLN2", "ubiquilin 2": "UBQLN2",
# SQSTM1 / p62
"p62": "SQSTM1", "sequestosome-1": "SQSTM1", "sequestosome 1": "SQSTM1",
# Profilin
"profilin 1": "PFN1", "profilin-1": "PFN1",
# Dynactin
"dynactin": "DCTN1", "dynactin 1": "DCTN1",
# Matrin
"matrin 3": "MATR3", "matrin-3": "MATR3",
# Other
"angiogenin": "ANG",
"senataxin": "SETX",
}
# ── Compound alias table ───────────────────────────────────────────────────────
_COMPOUND_ALIASES: dict[str, str] = {
"riluzole": "riluzole", "Riluzole": "riluzole",
"edaravone": "edaravone", "Edaravone": "edaravone", "MCI-186": "edaravone",
"tofersen": "tofersen", "Tofersen": "tofersen",
"BIIB067": "tofersen", "biib067": "tofersen",
"AMX0035": "AMX0035", "amx0035": "AMX0035",
"sodium phenylbutyrate": "AMX0035",
"tauroursodeoxycholic acid": "AMX0035",
"TUDCA": "AMX0035",
"masitinib": "masitinib", "Masitinib": "masitinib", "AB1010": "masitinib",
"bosutinib": "bosutinib", "Bosutinib": "bosutinib", "SKI-606": "bosutinib",
"mexiletine": "mexiletine", "Mexiletine": "mexiletine",
"memantine": "memantine", "Memantine": "memantine",
"rasagiline": "rasagiline", "Rasagiline": "rasagiline",
"NurOwn": "NurOwn", "MSC-NTF": "NurOwn",
"ozanezumab": "ozanezumab",
}
# ── Mechanism normalization ────────────────────────────────────────────────────
_MECHANISM_ALIASES: dict[str, str] = {
"glutamate excitotoxicity": "glutamate_excitotoxicity",
"excitotoxicity": "glutamate_excitotoxicity",
"glutamatergic excitotoxicity": "glutamate_excitotoxicity",
"oxidative stress": "oxidative_stress",
"reactive oxygen species": "oxidative_stress",
"ROS": "oxidative_stress",
"neuroinflammation": "neuroinflammation",
"microglial activation": "neuroinflammation",
"astrocyte activation": "neuroinflammation",
"protein aggregation": "protein_aggregation",
"protein misfolding": "protein_aggregation",
"protein inclusions": "protein_aggregation",
"RNA metabolism": "RNA_metabolism_dysfunction",
"RNA processing": "RNA_metabolism_dysfunction",
"RNA-binding protein dysfunction": "RNA_metabolism_dysfunction",
"stress granules": "RNA_metabolism_dysfunction",
"mitochondrial dysfunction": "mitochondrial_dysfunction",
"mitochondrial impairment": "mitochondrial_dysfunction",
"axonal transport": "axonal_transport_defect",
"axonal transport defect": "axonal_transport_defect",
"autophagy": "autophagy_impairment",
"mitophagy": "autophagy_impairment",
"ubiquitin proteasome": "autophagy_impairment",
"TDP-43 pathology": "TDP43_pathology",
"TDP-43 aggregation": "TDP43_pathology",
"TDP-43 mislocalization": "TDP43_pathology",
"antisense oligonucleotide": "antisense_oligonucleotide",
"ASO": "antisense_oligonucleotide",
"gene therapy": "gene_therapy",
"stem cell": "stem_cell_therapy",
"neurodegeneration": "neurodegeneration",
"apoptosis": "apoptosis",
"DNA damage": "DNA_damage_repair",
}
def guess_entity_type(name: str) -> str:
"""Best-effort entity type inference from name. Used when type metadata is unavailable."""
n = name.strip()
if n.upper() in _GENE_ALIASES or n in _GENE_ALIASES:
return "Gene"
if n in _COMPOUND_ALIASES:
return "Compound"
return "Protein"
def normalize_entity(name: str, entity_type: str) -> str:
"""Return a canonical_id string in the form '<prefix>:<canonical_name>'."""
canonical = _resolve_name(name.strip(), entity_type)
return f"{_prefix(entity_type)}:{canonical}"
def _resolve_name(name: str, entity_type: str) -> str:
t = entity_type.lower()
if t == "gene":
return _GENE_ALIASES.get(name) or _GENE_ALIASES.get(name.upper()) or name.upper()
if t == "protein":
gene_hit = _GENE_ALIASES.get(name) or _GENE_ALIASES.get(name.upper())
if gene_hit:
return gene_hit
return name[0].upper() + name[1:] if name else name
if t == "compound":
return _COMPOUND_ALIASES.get(name) or _slugify(name)
if t == "mechanism":
return _MECHANISM_ALIASES.get(name) or _slugify(name)
return _slugify(name)
def _prefix(entity_type: str) -> str:
return {
"gene": "gene",
"protein": "protein",
"compound": "compound",
"pathway": "pathway",
"phenotype": "phenotype",
"mechanism": "mechanism",
}.get(entity_type.lower(), "entity")
def _slugify(text: str) -> str:
return re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
class CanonicalRegistry:
"""Persistent name→canonical_id mapping written to canonical_ids.json."""
def __init__(self, path: Path = CANONICAL_IDS_PATH) -> None:
self.path = path
self._data: dict[str, str] = {}
if path.exists():
self._data = json.loads(path.read_text())
def resolve(self, name: str, entity_type: str) -> str:
key = f"{entity_type.lower()}:{name}"
if key not in self._data:
self._data[key] = normalize_entity(name, entity_type)
return self._data[key]
def save(self) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps(self._data, indent=2, sort_keys=True))
def __len__(self) -> int:
return len(self._data)