Skip to content

Commit 3551a7b

Browse files
Enrich the labeled set with PubChem names alongside BP/VP (#74)
build_properties.py now pulls the common (Title) and IUPAC names from PubChem's property endpoint in the same pass as boiling point / vapor pressure, writing them into properties.parquet. App-side, _names() reads this precomputed table first and falls back to live PubChem only on a miss, so substitution / palette / neighbor reads resolve names instantly and offline across the whole labeled set instead of one on-demand PubChem call per result. Names and experimental properties are both public domain, so this stays commercial-clean. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent f70ec1e commit 3551a7b

2 files changed

Lines changed: 65 additions & 19 deletions

File tree

training/app.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,36 @@ def _svg(smi, w=320, h=220):
6262
return None
6363

6464

65+
def _load_name_table():
66+
"""inchikey-skeleton -> (common, IUPAC) from the precomputed enrichment table, so the
67+
whole labeled set resolves instantly and offline. Empty until build_properties.py has
68+
written name columns; live PubChem stays the fallback for anything not in the table."""
69+
try:
70+
import pandas as pd
71+
df = pd.read_parquet("properties.parquet")
72+
if "common_name" not in df.columns:
73+
return {}
74+
out = {}
75+
for ik, c, u in zip(df["inchikey"], df["common_name"], df["iupac_name"]):
76+
if isinstance(ik, str) and (isinstance(c, str) or isinstance(u, str)):
77+
out[ik.split("-")[0]] = (c if isinstance(c, str) else None,
78+
u if isinstance(u, str) else None)
79+
return out
80+
except Exception: # noqa: BLE001 — no table / no pandas; just fall back to live lookups
81+
return {}
82+
83+
84+
_NAME_TABLE = _load_name_table()
85+
86+
6587
@lru_cache(maxsize=8192)
6688
def _names(smi):
67-
"""(common, IUPAC) names from PubChem for a SMILES — cached, best-effort, short timeout."""
89+
"""(common, IUPAC) names — from the precomputed table first (instant), else live PubChem."""
90+
mol = Chem.MolFromSmiles(smi) if smi else None
91+
if mol is not None:
92+
hit = _NAME_TABLE.get(Chem.MolToInchiKey(mol).split("-")[0])
93+
if hit:
94+
return hit
6895
import json
6996
import urllib.parse
7097
import urllib.request

training/build_properties.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"""
2-
build_properties.py — measured boiling point / vapor pressure from PubChem.
2+
build_properties.py — measured properties + identity from PubChem.
33
4-
PubChem experimental properties are **public domain** (no commercial restriction), so
5-
this is a commercial-clean source. It pulls each molecule's experimental Boiling Point
4+
PubChem experimental properties AND compound identity are **public domain** (no commercial
5+
restriction), so this is a commercial-clean source. For each molecule it pulls the common
6+
name (Title) and IUPAC name from the property endpoint, plus the experimental Boiling Point
67
(and, best-effort, Vapor Pressure) from PUG-View, parses to °C / Pa, and writes
7-
properties.parquet — the lookup table predict.py reads for MEASURED volatility. We use
8-
a measured lookup here on purpose: structure-based BP (Joback) was evaluated and
9-
rejected (~90 °C error), so a number is only ever reported when it's a real measurement.
8+
properties.parquet — the enrichment table predict.py reads for MEASURED volatility and the
9+
demo reads for instant offline names. We use a measured lookup for BP on purpose: structure-
10+
based BP (Joback) was evaluated and rejected (~90 °C error), so a number is only ever
11+
reported when it's a real measurement. Names default to live PubChem in the app; precomputing
12+
them here makes the substitution / palette / neighbor reads instant for the whole labeled set.
1013
1114
Usage:
1215
python build_properties.py # default: taste_master.parquet
@@ -125,12 +128,21 @@ def parse_vp_pa(strings):
125128
return round(vals[len(vals) // 2], 2) if vals else None
126129

127130

131+
def _names_for_cid(cid):
132+
"""(common Title, IUPAC) from PubChem's property endpoint — public-domain identity."""
133+
d = _get(f"{_BASE}/pug/compound/cid/{cid}/property/Title,IUPACName/JSON")
134+
p = ((d or {}).get("PropertyTable", {}).get("Properties") or [{}])[0]
135+
return p.get("Title"), p.get("IUPACName")
136+
137+
128138
def fetch_props(inchikey):
129139
cid = _cid(inchikey)
130140
if not cid:
131-
return None, None, None
141+
return None, None, None, None, None
132142
bp, bp_press = parse_bp(_heading_strings(cid, "Boiling Point"))
133-
return bp, bp_press, parse_vp_pa(_heading_strings(cid, "Vapor Pressure"))
143+
vp = parse_vp_pa(_heading_strings(cid, "Vapor Pressure"))
144+
common, iupac = _names_for_cid(cid)
145+
return bp, bp_press, vp, common, iupac
134146

135147

136148
def load_keys(path):
@@ -165,9 +177,10 @@ def load_keys(path):
165177
print(f"{smi}: unparseable")
166178
continue
167179
ik = Chem.MolToInchiKey(mol)
168-
bp, bp_press, vp = fetch_props(ik)
180+
bp, bp_press, vp, common, iupac = fetch_props(ik)
169181
cond = f" @{bp_press}mmHg" if bp_press else ""
170-
print(f"{smi:32s} ik={ik} bp_c={bp}{cond} vp_pa={vp}")
182+
print(f"{smi:32s} ik={ik} name={common!r} iupac={iupac!r} "
183+
f"bp_c={bp}{cond} vp_pa={vp}")
171184
time.sleep(0.3)
172185
sys.exit(0)
173186

@@ -177,19 +190,25 @@ def load_keys(path):
177190
sys.exit(1)
178191
keys = load_keys(src)
179192
rows = []
193+
cols = ["inchikey", "common_name", "iupac_name", "boiling_point_c",
194+
"boiling_point_pressure_mmhg", "vapor_pressure_pa"]
180195
for i, ik in enumerate(keys):
181-
bp, bp_press, vp = fetch_props(ik)
182-
if bp is not None or vp is not None:
183-
rows.append({"inchikey": ik, "boiling_point_c": bp,
184-
"boiling_point_pressure_mmhg": bp_press, "vapor_pressure_pa": vp})
196+
bp, bp_press, vp, common, iupac = fetch_props(ik)
197+
if bp is not None or vp is not None or common or iupac:
198+
rows.append({"inchikey": ik, "common_name": common, "iupac_name": iupac,
199+
"boiling_point_c": bp, "boiling_point_pressure_mmhg": bp_press,
200+
"vapor_pressure_pa": vp})
185201
if (i + 1) % 50 == 0:
186-
print(f" {i + 1}/{len(keys)} processed, {len(rows)} with measured props")
202+
named = sum(1 for r in rows if r["common_name"])
203+
print(f" {i + 1}/{len(keys)} processed, {len(rows)} enriched ({named} named)")
187204
time.sleep(0.3)
188-
new = pd.DataFrame(rows, columns=["inchikey", "boiling_point_c",
189-
"boiling_point_pressure_mmhg", "vapor_pressure_pa"])
205+
new = pd.DataFrame(rows, columns=cols)
190206
if Path(a.out).exists(): # accumulate across molecule sets
191207
old = pd.read_parquet(a.out)
192208
new = pd.concat([old, new], ignore_index=True).drop_duplicates("inchikey", keep="last")
209+
new = new.reindex(columns=cols) # keep a stable schema as older tables get names
193210
new.to_parquet(a.out)
194-
print(f"{a.out}: {len(new)} molecules total with measured BP/VP "
211+
named = int(new["common_name"].notna().sum())
212+
print(f"{a.out}: {len(new)} molecules total (names: {named}, "
213+
f"BP: {int(new['boiling_point_c'].notna().sum())}) "
195214
f"(+{len(rows)} from {src}; public-domain PubChem data)")

0 commit comments

Comments
 (0)