将标准化分子读为图
解析真实 V2000 记录,保留形式电荷,比较连通分量与环,并导出可检查的图。
目标与运行方法
本课适合具备 Python 基础的本科生。先阅读每一步的方法与图注,再修改参数。AI 练习为可选部分,不需要 AI 账号即可完成核心课程。数据写在 notebook metadata 中,不需要另下载数据文件。请保持 notebook 已保存到当前工作文件夹。
学习任务是理解本课的研究问题、执行透明的计算、校验结果,并说明结论的边界。
环境与参数
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
import networkx as nx
from IPython.display import display as _display
def display(value):
if isinstance(value,pd.DataFrame):
table=value.to_html(max_rows=12,escape=True)
style="<style>.vibeit-readable-table{max-width:100%;overflow-x:auto}.vibeit-readable-table table{width:max-content;max-width:none;border-collapse:collapse}.vibeit-readable-table td,.vibeit-readable-table th{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important}</style>"
return _display(HTML(style+'<div class="vibeit-readable-table">'+table+'</div>'))
return _display(value)
RECIPE_ID='ch02-molecular-graphs'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
"axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6 Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads Core data mode: embedded snapshot; no network requests
读取内嵌快照
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
"""Read embedded data from this notebook, including a renamed copy."""
for path in sorted(Path.cwd().glob("*.ipynb")):
try:
notebook = json.loads(path.read_text(encoding="utf-8"))
metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
blob = metadata.get("snapshots", {}).get(snapshot_key)
if metadata.get("recipe_id") != recipe_id or not blob:
continue
if blob.get("sha256") != expected_sha256:
continue
raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
if hashlib.sha256(raw).hexdigest() != expected_sha256:
raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
return json.loads(raw)
except (OSError, json.JSONDecodeError):
continue
raise FileNotFoundError(
"Save/open the complete .ipynb in the active working folder. "
"The embedded snapshot could not be located for " + recipe_id)
SNAPSHOT_HASHES={'molecules': '780c5f98bb3b7261465f8330d4b31589d4224cdf21f916b4a1788f11bcc00aac'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['molecules']
方法函数
def parse_molfile(text):
"""Read V2000 atoms/bonds and formal-charge annotations from one SDF record."""
lines=text.splitlines()
if len(lines)<4 or 'V2000' not in lines[3]:raise ValueError('Expected one V2000 molfile/SDF record')
atom_count=int(lines[3][:3]);bond_count=int(lines[3][3:6])
if atom_count<=0 or len(lines)<4+atom_count+bond_count:raise ValueError('Truncated molfile')
atoms=[];charge_codes={0:0,1:3,2:2,3:1,5:-1,6:-2,7:-3}
for j,line in enumerate(lines[4:4+atom_count]):
element=line[31:34].strip();position=[float(line[:10]),float(line[10:20]),float(line[20:30])]
if not element or not np.isfinite(position).all():raise ValueError('Invalid atom record')
charge_code=int(line[36:39].strip() or 0)
if charge_code not in charge_codes:raise ValueError('Unsupported atom charge/radical code')
atoms.append(dict(index=j,element=element,position=position,formal_charge=charge_codes[charge_code]))
bonds=[];seen=set()
for line in lines[4+atom_count:4+atom_count+bond_count]:
a=int(line[:3])-1;b=int(line[3:6])-1;order=int(line[6:9]);pair=tuple(sorted([a,b]))
if not 0<=a<atom_count or not 0<=b<atom_count or a==b or pair in seen or order not in [1,2,3,4]:
raise ValueError('Invalid or duplicate bond')
seen.add(pair);bonds.append(dict(a=a,b=b,order=order))
for line in lines[4+atom_count+bond_count:]:
if line.startswith('M CHG'):
fields=line.split();count=int(fields[2])
for j in range(count):
index=int(fields[3+2*j])-1;charge=int(fields[4+2*j])
if not 0<=index<atom_count:raise ValueError('Invalid formal-charge atom index')
atoms[index]['formal_charge']=charge
return dict(atoms=atoms,bonds=bonds,title=lines[0],atom_count=atom_count,bond_count=bond_count)
print("Method ready: parse_molfile")
Method ready: parse_molfile
def molecular_graph(parsed):
graph=nx.Graph()
for atom in parsed['atoms']:graph.add_node(atom['index'],element=atom['element'],formal_charge=atom['formal_charge'])
for bond in parsed['bonds']:graph.add_edge(bond['a'],bond['b'],reported_bond_type=bond['order'])
components=nx.number_connected_components(graph)
return graph,dict(atoms=graph.number_of_nodes(),bonds=graph.number_of_edges(),connected_components=components,cycle_rank=graph.number_of_edges()-graph.number_of_nodes()+components)
print("Method ready: molecular_graph")
Method ready: molecular_graph
数据与模型边界
PubChem 性质与构象是数据库计算字段,不是新实验测量。本课 V2000 解析范围明确限定为原子、来源键型与形式电荷。滴定及校准使用已声明的理想/模拟教学假设;水蒸气压使用署名的 IAPWS 参考关联式。描述符、距离和视觉邻近不能证明生物活性或结合强度。
分步分析
1. 读取原子、键与电荷记录
V2000 数量行定义记录边界。原子 ID 转为 Python 零基索引,检查键端点,拒绝重复/自身边。保留形式电荷,不根据颜色猜测。
molecules=snapshot("molecules")
properties=pd.DataFrame(molecules["properties"]).set_index("CID")
assert len(properties)==8 and properties.index.is_unique
display(properties[["name","MolecularFormula","MolecularWeight","TPSA","Charge"]])
parsed={int(cid):parse_molfile(molecules["sdf_2d"][str(cid)]) for cid in properties.index}
graphs={};summaries=[]
for cid,record in parsed.items():
graph,summary=molecular_graph(record);graphs[cid]=graph
summaries.append({"CID":cid,"name":properties.loc[cid,"name"],**summary,"formal_charge":sum(a["formal_charge"] for a in record["atoms"])})
summary=pd.DataFrame(summaries).set_index("CID");display(summary)
| name | MolecularFormula | MolecularWeight | TPSA | Charge | |
|---|---|---|---|---|---|
| CID | |||||
| 702 | ethanol | C2H6O | 46.070 | 20.2 | 0 |
| 176 | acetic acid | C2H4O2 | 60.050 | 37.3 | 0 |
| 241 | benzene | C6H6 | 78.110 | 0.0 | 0 |
| 887 | methanol | CH4O | 32.042 | 20.2 | 0 |
| 180 | acetone | C3H6O | 58.080 | 17.1 | 0 |
| 2519 | caffeine | C8H10N4O2 | 194.190 | 58.4 | 0 |
| 2244 | aspirin | C9H8O4 | 180.160 | 63.6 | 0 |
| 750 | glycine | C2H5NO2 | 75.070 | 63.3 | 0 |
| name | atoms | bonds | connected_components | cycle_rank | formal_charge | |
|---|---|---|---|---|---|---|
| CID | ||||||
| 702 | ethanol | 9 | 8 | 1 | 0 | 0 |
| 176 | acetic acid | 8 | 7 | 1 | 0 | 0 |
| 241 | benzene | 12 | 12 | 1 | 1 | 0 |
| 887 | methanol | 6 | 5 | 1 | 0 | 0 |
| 180 | acetone | 10 | 9 | 1 | 0 | 0 |
| 2519 | caffeine | 24 | 25 | 1 | 2 | 0 |
| 2244 | aspirin | 21 | 21 | 1 | 1 | 0 |
| 750 | glycine | 10 | 9 | 1 | 0 | 0 |
2. 展示来源分子图
用苯的二维 SDF 排布绘图,这些排布坐标不是实验键长。节点颜色区分元素,边标签为来源键型代码,不是力常数或相互作用能。
cid=241;record=parsed[cid];graph=graphs[cid]
positions={a["index"]:a["position"][:2] for a in record["atoms"]}
labels={a["index"]:a["element"]+str(a["index"]) for a in record["atoms"]}
fig,ax=plt.subplots(figsize=(7,6));nx.draw_networkx(graph,pos=positions,labels=labels,node_size=600,node_color=["#b4d4ed" if graph.nodes[j]["element"]=="C" else "#eeeeee" for j in graph],ax=ax,font_size=9)
nx.draw_networkx_edge_labels(graph,positions,edge_labels=nx.get_edge_attributes(graph,"reported_bond_type"),ax=ax,font_size=8)
ax.set_title("Benzene: reported 2D molecular graph");ax.axis("off");plt.tight_layout();plt.show()
3. 比较独立环数量
无向图的环秩为边数−节点数+连通分量,计数独立图环,不判断芳香性或立体化学环。显式氢节点影响度数,但不改变该拓扑恒等式。
fig,ax=plt.subplots(figsize=(8,4));ax.barh(summary.name,summary.cycle_rank)
ax.set(xlabel="Independent cycle rank",title="Topology from reported atom/bond records");plt.tight_layout();plt.show()
4. 检查节点度数
直方图使用八个记录中的全部原子,包括显式氢。图度数计邻居,与键级和、化学价不同。本解析器不承担完整化学工具库的功能。
degrees=np.concatenate([np.array([degree for node,degree in graph.degree()]) for graph in graphs.values()])
fig,ax=plt.subplots(figsize=(7,4));ax.hist(degrees,bins=np.arange(-.5,6.6,1),edgecolor="white")
ax.set(xlabel="Reported atom-neighbor degree",ylabel="Atoms",title="Degree counts across declared structures");plt.tight_layout();plt.show()
5. 核对数量并导出图记录
将各图原子/键数与来源头逐个核对,并将总形式电荷与性质接口比较。导出一个图及完整摘要。校验依靠记录身份,不能只看图形。
for cid,record in parsed.items():
assert graphs[cid].number_of_nodes()==record["atom_count"] and graphs[cid].number_of_edges()==record["bond_count"]
assert summary.loc[cid,"formal_charge"]==properties.loc[cid,"Charge"]
assert summary.loc[241,"cycle_rank"]==1
OUTPUT_DIR.mkdir(parents=True,exist_ok=True);summary.to_csv(OUTPUT_DIR/"molecular-graph-summary.csv")
nx.write_graphml(graphs[241],OUTPUT_DIR/"benzene.graphml")
print("Header, graph and charge checks passed; exports:",OUTPUT_DIR)
Header, graph and charge checks passed; exports: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads/results/ch02-molecular-graphs
自主练习与参考答案
- 区分图度数、键型代码与价。
- 解释苯的环秩。
参考解释。 度数计邻居,来源键型代码不是能量。连通的苯图有一个独立拓扑环。
先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。
可选的 AI 编程练习
提示词 1
仅用 NumPy、Pandas、Matplotlib、NetworkX 绘制另一个声明 CID,保留原子 ID 与形式电荷。
提示词 2
使用同样的包将各声明图导出为 GraphML,仅使用标量属性。
人工验收。 头数量、总电荷和图不变量一致,重复/自身键报错。
检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。
可选联网扩展
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/2519/property/MolecularFormula/JSON',timeout=20)
response.raise_for_status()
print("Source:",response.url)
print(response.text[:700])
except requests.RequestException as error:
print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged
来源、快照与下一步
PubChem calculated molecular properties — NCBI public data; PubChem computed properties only; credit PubChem; no third-party narrative annotations included; retrieved 2026-10-09. SHA-256
b01b092cc8872e14fdab1fdc608c783669131a081c71d9ccee77c0bb9c6ff408. Keep the returned calculated fields for eight declared CIDs; molecular weight converted from API numeric string.PubChem 2D SDF 702 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
5962afc9ee8115dee9d5e63cefeef89cdc47641420b34afe156062d263c9caa7. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 176 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
f51353e1db0a0be8aa3aeb7bea28b6febb1e1f8afdf88fbf298405851f7ee092. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 241 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
3f4698bd6773f5e709184759048c116d106ac392e874676444b64af254e9a189. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 887 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
5cf6d180935f2fe77d20772a4bf2506711d4436762f6c6801157ac748f088530. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 180 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
6ecceacce326b6ecc292dedd946f589e879c999f43242395d995b59a8c8eb128. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 2519 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
47293391d630f44f2a32c3f7702d766744c8f4a4f16f6571d4ae09f04b3daaed. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 2244 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
0dfd04496745032c06410241f91f10fcec77cbc6828cdbede75f4c08da418fbb. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 750 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
e1c2ddd732cea1d052a9426c9a25fef86acd1309748bb2115cf2b56481ba288f. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem caffeine 3D conformer — NCBI public data; PubChem computed conformer; credit PubChem; retrieved 2026-10-09. SHA-256
2a78d98d09d50768fe3f6e6114d7baf3dad9cf88e41835735f96abf5867f5092. Preserve calculated coordinates and conformer identifiers; not an experimental crystal structure.
下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。