docs(validation): add macrolactone fragmentation report

This commit is contained in:
hotwa
2026-03-24 22:35:05 +08:00
parent 3e07402f4e
commit 8478abbbaa
3 changed files with 650 additions and 3 deletions

View File

@@ -1,11 +1,205 @@
# 大环内酯碎片库分析报告(中文)
## 数据筛选流程图
下面这张图把本报告里的数据是如何一步步筛出来的串起来。它分成两段:
- 第一段是 `validation` 阶段,负责把原始 MacrolactoneDB 母体变成 `fragment_library.csv`
- 第二段是 `fragment_library_analysis` 阶段,负责从 `fragment_library.csv` 里统计出本报告里的 34,829、4,451、8,108、1,105 等数字
```mermaid
flowchart TD
A["原始输入 CSV<br/>/Users/lingyuzeng/project/macro_split/data/MacrolactoneDB/ring12_20/temp.csv"] --> B["stratified_sample_by_ring_size()<br/>按 12-20 元环分层抽样 10%"]
B --> C["classify_macrocycle()<br/>只保留 standard_macrolactone"]
C -->|non_standard_macrocycle / not_macrolactone| C1["processing_status = skipped<br/>不进入裂解"]
C -->|standard_macrolactone| D["find_macrolactone_candidates()<br/>确认主环候选"]
D --> E["number_macrolactone()<br/>建立 canonical numbering"]
E --> F["遍历 position > 2 的环位点"]
F --> G["跳过 ring atom<br/>跳过 intrinsic lactone neighbor"]
G --> H["collect_fragmentable_side_chain_atoms()<br/>只保留可拼接单锚点侧链"]
H -->|None| H1["丢弃该邻居/该裂解尝试"]
H -->|atoms returned| I["build_fragment_with_isotope()<br/>生成同位素标记碎片"]
I --> J["Chem.MolFromSmiles(plain_smiles)<br/>SMILES 必须有效"]
J -->|invalid| J1["丢弃该碎片"]
J -->|valid| K["写入 SideChainFragment"]
K --> L["写入 FragmentLibraryEntry<br/>source_type='validation_extract'<br/>splice_ready=True"]
L --> M["parent.processing_status = success<br/>num_sidechains += 1"]
M --> N["导出 fragment_library.csv / summary.csv / fragments.db"]
N --> O["load_fragment_library_dataset()<br/>把 fragment_library.csv 与 parent_molecules 合并"]
O --> P["annotate_fragment_atom_counts()<br/>按 plain SMILES 计算重原子数"]
P --> Q["build_fragment_atom_count_summary()<br/>得到 34,829 / 4,451 / 1,852"]
P --> R["build_position_diversity_table()<br/>得到位点统计表"]
P --> S["ring_size=16 过滤<br/>得到 8,108 条片段、1,105 个母体"]
S --> T["> 3 重原子过滤<br/>得到设计相关子集与位点多样性结论"]
```
这张流程图对应的是一条从“原始母体”到“最终碎片统计”的两段式筛选链路。这里的“原始输入 CSV”指的是传入 `validator.run(input_csv)` 的 MacrolactoneDB ring12_20 数据集文件,在当前仓库里的默认入口脚本 [scripts/validate_macrolactone_db.py](/Users/lingyuzeng/project/macro_split/scripts/validate_macrolactone_db.py) 中,默认值就是 `data/MacrolactoneDB/ring12_20/temp.csv`,对应的绝对路径是 `/Users/lingyuzeng/project/macro_split/data/MacrolactoneDB/ring12_20/temp.csv`。第一段发生在 `validation` 阶段:程序先读取这个输入 CSV对所有分子做 `classify_macrocycle()` 预分类,并按 12 到 20 元环分层抽样。这里的“抽 10%”不是对整个数据集一次性随机抽样,而是先按 `_ring_size` 把分子分到 12、13、14、15、16、17、18、19、20 这九个层级,再对每个层级分别抽取 `max(1, int(len(group) * sample_ratio))` 条记录;`sample_ratio` 的默认值是 `0.1``random_state` 的默认值是 `42`。抽样之后,只有被识别为 `standard_macrolactone` 的分子才会继续处理;`non_standard_macrocycle``not_macrolactone` 会直接被标记为 `skipped`,不会进入裂解步骤。对标准大环内酯,程序先用 `find_macrolactone_candidates()` 确认主环候选,再用 `number_macrolactone()` 建立 canonical numbering之后只遍历 `position > 2` 的环位点。对于每个位点,代码会跳过环内原子和 intrinsic lactone neighbor只对真正位于外侧链上的邻居进行处理随后通过 `collect_fragmentable_side_chain_atoms()` 判断这段侧链是否满足“单锚点、可拼接”的要求。如果该函数返回 `None`,说明这条侧链不符合拼接条件,整次裂解尝试就会被丢弃。若侧链可拼接,则继续用 `build_fragment_with_isotope()` 生成同位素标记碎片,并用 `Chem.MolFromSmiles(plain_smiles)` 做一次有效性检查;只有 SMILES 能被正确解析的碎片才会被写入 `SideChainFragment``FragmentLibraryEntry`。在写入统一碎片库时,这些记录会被固定标记为 `source_type='validation_extract'``splice_ready=True`,表示它们是后续拼接设计可以直接使用的碎片。母体记录则会被回写为 `processing_status = success`,并记录该分子实际产生了多少条侧链碎片以及对应的裂解位点。
第二段发生在 `fragment_library_analysis` 阶段:分析脚本只读取已经生成好的 `fragment_library.csv`,再从 SQLite 数据库中读取 `parent_molecules` 表,把 `source_parent_ml_id``ml_id` 对齐后合并母体元数据。这个合并步骤的作用是把每条碎片重新挂回到它来自哪个母体、属于哪种分类、对应哪个环大小以及母体是否成功处理等信息上;如果有任何碎片在数据库里找不到对应母体,分析会直接报错,而不会静默继续。合并完成后,脚本会根据 `fragment_smiles_plain` 计算重原子数,形成后续所有统计的基础字段。全库的 `34,829` 条记录和 `4,451` 个去重母体,就是在这一步对 `fragment_library_entries` 做总量统计得到的;随后再按 `ring_size=16` 过滤,就得到 16 元环子集的 `8,108` 条碎片记录和 `1,105` 个母体。接下来如果进一步应用 `>= 4` 重原子的设计相关过滤,就会得到用于位点多样性分析的严格子集;`build_position_diversity_table()` 就是在这个子集上按 `cleavage_position` 分组统计每个位点的总碎片数、唯一碎片数、香农熵、Tanimoto 距离和原子数分布,从而得出本报告里 16 元环位点排序和热点结论。
### 标准大环内酯的识别规则
这里再单独把“标准环怎么判”的逻辑说清楚,因为这是整个筛选链路的根。
- 入口是 [analyzer.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/analyzer.py#L19-L26) 调用 [\_core.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/_core.py#L89-L137) 里的 `classify_macrolactone()`
- `find_macrolactone_candidates()` 会遍历 `mol.GetRingInfo().AtomRings()`,只保留环长在 `12..20` 的候选环
- 每个候选环都必须能找到一个乳内酯特征:
- 1 个羰基碳
- 1 个酯氧
- 能组成 lactone ring 的闭环
- 如果找不到任何 12-20 元 lactone ring就会被判为 `not_macrolactone`
- 如果找到多个彼此重叠的候选环,就会被判为 `non_standard_macrocycle`,这是桥环、稠环或多环重叠结构的主要排除门槛
- 如果候选环唯一,再调用 `build_numbering_result()` 建立编号,随后检查 `3..N` 位是否存在非碳原子:
- 只要环上除 1、2 位以外出现了 O、N 等非 C 原子,就会被判为 `non_standard_macrocycle`
- 对应的原因码是 `contains_non_carbon_ring_atoms_outside_positions_1_2`
- 只有“候选环唯一 + 3..N 全为碳 + 环长在 12-20”同时成立时才会被判为 `standard_macrolactone`
这也解释了你提到的两类结构:
- **环肽/非标准大环**:如果环上 3..N 出现 N、O 等非碳原子,虽然可能也是大环,但不会进入标准大环内酯集合
- **桥环/稠环**:如果一个分子里存在多个重叠的 lactone 候选环,程序会直接把它归为 `non_standard_macrocycle`,不会进入后续裂解
### 口径对照
- `34,829` 是最终 `fragment_library_entries` 中的总行数,不是原始输入总分子数
- `4,451` 是这 34,829 条片段对应的去重母体数
- `4,482``summary.csv` / 数据库里所有 `standard_macrolactone + success` 母体数
- 两者之差 `31` 个母体,是已经成功识别为标准大环内酯、但 `num_sidechains=0`,因此没有产生任何可拼接片段的分子
- `1,111``standard_macrolactone + ring_size=16 + success` 母体数
- `1,105` 是其中至少产出 1 条可拼接片段的 16 元环母体数
- 两者之差 `6` 个母体,也是没有任何可拼接外侧链的 16 元环分子
## 分步筛选说明(对应代码)
### 1. 输入与分层抽样
- 入口在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L38-L67) 的 `run()`
- 原始输入先读成 `DataFrame`
- 然后调用 [sampling.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/sampling.py#L8-L60) 里的 `stratified_sample_by_ring_size(df, self.sample_ratio, self.smiles_col)`
- 这一步先对所有分子做 `classify_macrocycle()` 预分类,再按 `_ring_size``12..20` 各层分别抽样
- 默认 `sample_ratio=0.1``random_state=42`
- 这一步的意义是:按 12-20 元环大小分层抽样,而不是对整个库做一次性随机抽样
### 2. 母体分类
- 入口在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L72-L146) 的 `_process_molecule()`
-`self.analyzer.classify_macrocycle(smiles)` 把分子分成三类:
- `standard_macrolactone`
- `non_standard_macrocycle`
- `not_macrolactone`
- 只有 `standard_macrolactone` 会进入后续裂解
- 其他两类直接设为 `processing_status = skipped`
### 3. 标准大环内酯的候选确认与编号
- 入口在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L148-L186)
- 先调用 `find_macrolactone_candidates(mol, ring_size=parent.ring_size)` 找主环候选
- 如果候选为空,或同一分子里出现多个无法唯一确定的候选,会报错或终止
- 然后调用 `number_macrolactone(mol, ring_size=parent.ring_size)` 建立 canonical numbering
- 这里的编号规则就是本仓库统一规则:`1 = 内酯羰基碳``2 = 相邻酯氧``3..N` 顺着环唯一遍历
### 4. 逐位点裂解
- 入口仍在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L187-L264)
- 循环只看 `position > 2` 的环位点,`1``2` 不参与裂解
- 对每个位点:
- 先取该位点的环原子
- 再遍历其邻居
- 若邻居还是环内原子,则跳过
- 若是 intrinsic lactone neighbor也跳过
- 剩下的邻居才进入 `collect_fragmentable_side_chain_atoms(...)`
### 5. 侧链可拼接性过滤
- 这一步是最关键的筛选门槛,代码仍在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L206-L214)
- `collect_fragmentable_side_chain_atoms(...)` 必须返回一个有效的原子集合
- 如果返回 `None`,说明该侧链不满足“可拼接单锚点侧链”的条件,直接丢弃
- 这也是桥环、稠环、双锚点或其他不可拼接外侧链被排除的核心位置
### 6. 碎片构建与有效性检查
- 入口在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L216-L228)
-`build_fragment_with_isotope(...)` 生成:
- `fragment_smiles_labeled`
- `fragment_smiles_plain`
- `original_bond_type`
- 接着用 `Chem.MolFromSmiles(plain_smiles)` 再做一次有效性检查
- 如果 plain SMILES 无法解析,这条碎片也会被丢弃
### 7. 写入片段库
- 入口在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L230-L264)
- 每条通过筛选的碎片都会写入两个表:
- `SideChainFragment`
- `FragmentLibraryEntry`
- 其中 `FragmentLibraryEntry` 固定写入:
- `source_type='validation_extract'`
- `splice_ready=True`
- `dummy_atom_count=1`
- 也就是说,进入本报告的都是已经被判定为“可直接用于单锚点拼接”的碎片
### 8. 母体状态回写
- 入口在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L273-L279)
- 处理完成后,母体会被标记为 `processing_status = success`
- 同时写回:
- `num_sidechains = len(fragments)`
- `cleavage_positions = [...]`
- 如果标准大环内酯没有任何可拼接侧链,母体仍然会是 `success`,但 `num_sidechains=0`
### 9. 统一导出
- 入口在 [validator.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/validator.py#L280-L383)
- 这一步导出三类关键文件:
- `summary.csv`
- `summary_statistics.json`
- `fragment_library.csv`
- 其中本报告使用的核心输入是 `fragment_library.csv` 和数据库里的 `parent_molecules`
### 10. 报告统计
- 入口在 [fragment_library_analysis.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/fragment_library_analysis.py#L44-L69)
- `load_fragment_library_dataset()` 会:
- 读取 `fragment_library.csv`
- 从 SQLite 里读取 `parent_molecules`
-`source_parent_ml_id = ml_id` 合并
- 校验母体元数据不能缺失
- 然后 `annotate_fragment_atom_counts()` 会按 `fragment_smiles_plain` 计算重原子数
### 11. 汇总输出
- 入口在 [fragment_library_analysis.py](/Users/lingyuzeng/project/macro_split/src/macro_lactone_toolkit/validation/fragment_library_analysis.py#L72-L183)
- `build_fragment_atom_count_summary()` 生成全库统计,得到本报告中的:
- `rows = 34,829`
- `unique_parent_molecules = 4,451`
- `unique_fragment_smiles = 1,852`
- `build_filter_candidate_table()` 生成阈值删除表,因此报告里能看到 `<= 2``<= 3``<= 4` 等过滤候选
- `build_position_diversity_table()` 生成位点多样性表,因此报告里能看到 16 元环的位点排序与 `> 3` 重原子子集结论
## 数据范围
- 当前验证后的可拼接碎片库包含 **34,829** 条片段记录,来源于 **4,451** 个母体分子。
- 其中 16 元环子集包含 **8,108** 条片段记录,来源于 **1,105** 个母体分子。
- 这里的“可拼接碎片”指验证库中 `source_type='validation_extract'``splice_ready=1` 的单锚点侧链片段。桥环、稠环或任何具有多个环连接点的侧链都已经在生成阶段被排除,不会进入这份库。
- 16 元环子集是按母体元数据里的 `ring_size=16` 直接过滤出来的,不是从片段反推 ring size。
- 用于设计相关位点分析的严格子集定义为:片段重原子数 **>= 4**。
## 16 元环母体计数口径说明
- 在数据库里,`standard_macrolactone + ring_size=16 + processing_status=success` 一共有 **1,111** 个母体。
- 其中 **1,105** 个母体至少产出过 1 条可拼接片段,所以进入了当前报告的 16 元环片段统计。
- 剩余 **6** 个母体没有任何可拼接片段;它们的共同特征是 `num_sidechains=0``cleavage_positions=[]`
```text
ml_id num_sidechains cleavage_positions
ML00006860 0 []
ML00007029 0 []
ML00007030 0 []
ML00007031 0 []
ML00007032 0 []
ML00008015 0 []
```
## 全库碎片大小结论
- 默认清洗阈值建议使用 `<= 2` 重原子删除。该阈值会删除 **28,069** 条记录80.6%),但仅删除 **26** 个唯一片段1.4%)。
@@ -41,7 +235,7 @@
## 桥环 / 稠环干扰的敏感性分析
桥连或双锚点侧链不会进入当前片段库,因为断裂逻辑只保留与主环存在 **1 个连接点** 的侧链组件。也就是说,真正的 bridge / fused multi-anchor components 已被代码层面排除。
桥连或双锚点侧链不会进入当前片段库,因为断裂逻辑只保留与主环存在 **1 个连接点** 的侧链组件。也就是说,真正的 bridge / fused multi-anchor components 已被代码层面排除。上面那 6 个 16 元环母体并不是这类“被误收进来的桥环碎片”,而是根本没有任何可拼接外侧链,所以不会产生 fragment 行。
但是,需要额外区分另一类情况:**cyclic single-anchor side chains**。这类片段虽然只在一个位置连到主环,因此会被保留下来,但片段自身可能包含糖环、杂环或其他环状骨架,仍然会显著影响位点多样性排名。
@@ -110,4 +304,3 @@
- 16 元环位点多样性图:`ring16_position_diversity_gt3.png`
- 16 元环桥环/带环侧链敏感性图:`ring16_position_ring_sensitivity.png`
- 16 元环药化热点对比图:`ring16_medchem_hotspot_comparison.png`