Skip to content

化学结构标注与 OCSR 校正

校正 OCSR 输出,按 SMILES 或 SMARTS 标准答案为手绘结构评分,在分子上标记原子,再导出 InChIKey 和 SD 文件。

v2.13.0 新增

chemical_structure 标注方案为标注者提供一个结构编辑器,molecule 显示类型绘制一个小分子,标注者可以在上面选择原子。化学数据集的构建者把这两者配合起来做结构校正。在结构校正中,标注者把从专利或论文插图中识别出的结构与原图对照,修正识别错误的原子和键。化学教育研究者和教师用编辑器收集手绘结构,例如某个化合物名称所描述的结构,Potato 在服务器上把每份绘制结果与标准答案比对评分。审编人员用 molecule 显示类型配合 viewer_selection 方案标记原子,例如反应中心或代谢位点。每个答案存储为一个 molfile,并附上所绘结构的规范 SMILES;chemistry 导出把每位标注者的结构写入一个 CSV 文件和一个 SD 文件。

光学化学结构识别(optical chemical structure recognition,OCSR)把化学结构的图像转换为机器可读的结构。Potato 不运行任何 OCSR 模型,研究载入的是其他工具识别出的结构。SMILES 用一行简短的文本表示一个结构,例如乙酸写作 CC(=O)O。MDL molfile 是化学表格文件(chemical table file)家族中的连接表文本格式,SD 文件包含多个 molfile,每个 molfile 后面跟着带名称的数据字段。InChIKey 是 IUPAC InChI 标识符的 27 字符哈希值,同一结构的两种画法得到相同的 InChIKey。

  • 编辑器: OpenChemLib JS 9.25.0(BSD-3-Clause),随 Potato 一起提交。编辑器在画布上绘制,工具栏提供键、环、元素、电荷、立体楔形键、擦除和撤销。画布下方的文本框接受键入或粘贴的 SMILES 字符串或 molfile,这个文本框是用键盘操作编辑器的途径。
  • RDKit.js(可选): RDKit 的 JavaScript MinimalLib 2026.3.6(BSD-3-Clause)是一个 7 MB 的 WebAssembly 构建,没有随 Potato 提交。potato vendor fetch rdkit 会下载这个构建,并用 npm 注册表的完整性哈希校验下载内容。有了 RDKit.js,浏览器还会计算 RDKit 规范 SMILES、InChI 和 InChIKey,标出 RDKit 无法规整化(sanitize)的结构,molecule 显示类型也可以用 engine: rdkit 绘制。
  • chemistry 可选依赖: 在服务器上安装 rdkit Python 包。这个包负责按标准答案评分,chemistry 导出也用它为未使用 RDKit.js 保存的答案补上 InChI 和 InChIKey。
  • 无需 WebGL: 两个工具包都以二维方式绘制。
  • 离线: 结构来自条目本身或媒体目录中的文件,标准答案从不发送到浏览器。

不同安装方式下可用的功能如下:

功能仅 OpenChemLib加 RDKit.js加 chemistry 可选依赖
编辑器、molfile、OpenChemLib SMILES 与 ID code、分子式、化合价检查是是是
存储答案中的 RDKit SMILES、InChI 和 InChIKey否是导出时补上
绘制时的 RDKit 规整化检查否是否
使用 engine: rdkit 的 molecule 显示类型否是否
按 smiles、molfile 和 SMARTS 标准答案评分否否是
按 inchikey 标准答案评分否是,使用浏览器计算的 InChIKey是

服务器端评分需要这个可选依赖,RDKit.js 则需要单独获取:

bash
pip install 'potato-annotation[chemistry]'
potato vendor fetch rdkit     # optional, InChIKeys in the browser

获取完成后请重启服务器。缺少 RDKit.js 时,potato validate 会对每个保持 rdkit: auto 的 chemical_structure 方案发出警告,并给出获取命令。

配置

下面的配置用于 OCSR 校正。每个条目把原始插图和识别出的结构并排显示,标注者标记识别器识别错的原子,在编辑器中校正结构,并给出判定:

yaml
annotation_task_name: "Structure recognition check"
task_dir: .
media_directory: media
output_annotation_dir: annotation_output
data_files: [data/structures.json]
item_properties: {id_key: id, text_key: note}
 
instance_display:
  layout:
    direction: horizontal
  fields:
    - key: page_image             # the figure cropped from the patent or paper
      type: image
      label: "Source image"
    - key: ocsr_smiles            # what the OCSR tool produced
      type: molecule
      label: "Recognized structure"
      display_options:
        height: 300
 
user_config:
  allow_all_users: true
 
annotation_schemes:
  - annotation_type: viewer_selection
    name: wrong_atoms
    description: "Mark the atoms the recognizer got wrong"
    viewer_field: ocsr_smiles
    labels: ["wrong element", "wrong bond or charge", "wrong stereo"]
 
  - annotation_type: chemical_structure
    name: corrected
    description: "Correct the structure so it matches the source image"
    prefill_field: ocsr_smiles
    answer_field: true_smiles     # graded in the export, never sent to the page
    label_requirement: {required: true}
 
  - annotation_type: radio
    name: verdict
    description: "How good was the recognition?"
    labels: ["Correct", "Minor errors", "Wrong structure"]
    label_requirement: {required: true}

编辑器从 ocsr_smiles 开始。标注者原样保留的预填结构会成为保存的答案,并带有 changed: false,因此识别正确时不需要任何编辑。verdict 问题把已接受的结构与标注者从未检查过的条目区分开。Potato 会从页面收到的所有内容中删除 true_smiles 字段。在导出中,answer_correct 表示每个校正后的结构是否与 true_smiles 一致。把 answer_correct 与 changed 对照,可以区分漏掉的错误(未改动且错误)和引入的错误(改动过且错误)。

每个条目给出相对于 media_directory 的插图路径,并给出两个结构:

json
{"id": "fig1_aspirin", "note": "Compound 1 as printed.", "page_image": "fig1_aspirin.png",
 "ocsr_smiles": "CC(=O)Oc1ccccc1C(=O)N", "true_smiles": "CC(=O)Oc1ccccc1C(=O)O"}

根据名称绘制结构

名称到结构的练习只需要一个方案,以及每个条目的一个标准答案。下面的配置把化合物名称作为条目文本显示:

yaml
annotation_task_name: "Name to structure"
task_dir: .
output_annotation_dir: annotation_output
data_files: [data/names.json]
item_properties: {id_key: id, text_key: name}
 
user_config:
  allow_all_users: true
 
annotation_schemes:
  - annotation_type: chemical_structure
    name: drawn
    description: "Draw the structure this name describes"
    answer_field: expected
    rdkit: false                  # OpenChemLib only, no RDKit.js warning
    editor_height: 420
    label_requirement: {required: true}
json
{"id": "q2", "name": "(S)-2-aminopropanoic acid", "expected": "C[C@H](N)C(=O)O"}

学生可以用鼠标绘制,也可以键入 SMILES 字符串后按 Enter。设置 rdkit: false 后,页面只载入 OpenChemLib,不存储 InChIKey,由 chemistry 可选依赖在导出时为绘制结果评分。

在分子上标记原子

原子标记把 molecule 显示类型与 viewer_selection 方案配合使用。下面的配置请审编人员标记代谢位点,并用琥珀色绘制模型预测的位点作为参考:

yaml
annotation_task_name: "Sites of metabolism"
task_dir: .
output_annotation_dir: annotation_output
data_files: [data/molecules.json]
item_properties: {id_key: id, text_key: note}
 
instance_display:
  fields:
    - key: smiles
      type: molecule
      display_options:
        show_atom_numbers: true
        highlight_field: predicted_site   # a model's predicted atoms, drawn in amber
 
user_config:
  allow_all_users: true
 
annotation_schemes:
  - annotation_type: viewer_selection
    name: som
    description: "Mark each atom where the compound is metabolized"
    viewer_field: smiles
    labels: ["hydroxylation", "N-dealkylation", "glucuronidation"]
    min_selections: 1
    label_requirement: {required: true}
json
{"id": "m1", "note": "Predicted sites in amber.", "smiles": "CC(=O)Nc1ccc(O)cc1", "predicted_site": [0, 8]}

highlight_field 指定的条目字段存放从 0 开始的原子索引,可以是列表,也可以是 "0, 8" 这样的文本。高亮是固定的,审编人员选择原子时不受高亮影响。min_selections 与 label_requirement 同时设置时,审编人员至少选中一个原子之前,Next 一直不可用。

结构数据

结构字段存放 SMILES 字符串、molfile 文本,或媒体目录中某个 .mol、.sdf、.smi 或 .smiles 文件的名称。服务器读取 .sdf 文件的第一条记录,以及 .smi 文件第一个非空行的第一个词元(token)。结构文本和结构文件的大小上限为 1 MB。不是 molfile 却含有空格的值会被拒绝,因为 SMILES 字符串中没有空格。显示类型从不获取 URL,Potato 也拒绝媒体目录之外的文件路径。

选项

chemical_structure 选项默认值含义
prefill_field无存放 SMILES 字符串、molfile 或结构文件名的条目字段。编辑器从这个结构开始
prefill_formatautoauto、smiles 或 molfile。需要设置 prefill_field
answer_field无存放标准答案的条目字段,在 chemistry 导出中评分。必须与 prefill_field 不同,并会从页面收到的内容中删除
rdkitautoauto 在已安装 RDKit.js 时使用它。true 要求 RDKit.js,缺少该构建时 potato validate 和服务器启动都会失败。false 只载入 OpenChemLib,并关闭警告
require_validtrue存在化合价问题或 RDKit.js 无法规整化的结构会使条目保持未完成。仅在方案为必填时生效
allow_emptyfalse接受空画布作为答案
editor_height380编辑器的像素高度,取值 240 到 900
show_identifierstrue在编辑器下方显示 SMILES、分子式和 InChIKey
instructions无显示在问题下方的文本
label_requirement无{required: true} 使方案成为必填
molecule 显示选项默认值含义
engineopenchemlibopenchemlib 或 rdkit。使用 engine: rdkit 的配置在运行 potato vendor fetch rdkit 之前无法通过 potato validate
height320绘图高度,以像素或 CSS 长度表示
formatautoauto、smiles 或 molfile
show_atom_numbersfalse在每个原子旁边标出它的 molfile 编号
atom_listtrue在绘图下方显示原子按钮列表,这是用键盘进行选择的途径
highlight_field无存放从 0 开始的原子索引的条目字段,这些原子以固定高亮绘制
highlight_color#f59f00固定高亮的十六进制颜色
max_atoms500更大的分子不绘制,显示类型会给出说明。取值 1 到 5,000
allow_remotefalse接受此选项,但它对这个显示类型没有作用
viewer_selection 选项默认值含义
viewer_field必填instance_display 中 molecule 字段的 key
labels无选择的名称,写成字符串或 {name, color}。新的选择使用当前标签,每个原子都可以更换标签
min_selections无方案为必填时,选中的原子达到这个数量之前,条目保持未完成
max_selections无超过这个数量的选择会被拒绝,并给出提示
instructions无显示在问题下方的文本

标准答案

标准答案可以是一个单独的 SMILES 字符串、一个 molfile 或一个映射。映射中的所有部分都必须通过:

键通过条件
smiles、molfile 或 inchikey所绘结构的 InChIKey 等于标准答案的 InChIKey。三者给出其一
ignore_stereo: true加在结构型键上时,只比较 InChIKey 的第一个 14 字符段,该段编码连接关系。不检查立体化学和同位素标记
substructure所绘结构包含该 SMARTS 模式,或包含列表中的每个模式
not_substructure所绘结构不包含其中任何一个 SMARTS 模式

精确模式比较标准 InChIKey。标准 InChI 把某些互变异构体视为同一结构,因此 2-吡啶酮(2-pyridone)的绘制结果可以通过以 2-羟基吡啶(2-hydroxypyridine)为标准答案的评分。电荷会被计入,因此即使设置了 ignore_stereo,乙氧基负离子(ethoxide)也无法通过以乙醇为标准答案的评分。子结构型标准答案为答案的某个特征评分,例如用 {"substructure": "C(=O)[OH]"} 判断是否含有羧酸。

answer_field 的值可以是单独的 SMILES 字符串。金标准条目和培训条目在方案名称下写映射形式:

json
{"id": "gold_1", "gold_label": {"corrected": {"smiles": "CC(=O)Oc1ccccc1C(=O)O"}}}
{"id": "train_1", "correct_answers": {"drawn": {"substructure": "C(=O)[OH]"}}}

质量控制和培训阶段用与导出相同的比较方法为这些标准答案评分。编辑器只在标注阶段读取 prefill_field,因此培训条目打开时画布为空。

没有 rdkit Python 包时,smiles、molfile 或 SMARTS 标准答案会把每个答案都判为错误,服务器会记录一次缺少该包的日志。有一种情况仍然可用。inchikey 标准答案可以匹配由 RDKit.js 在标注者浏览器中计算出 InChIKey 的答案。安装了该包后,服务器会从存储的 molfile 重新计算 InChIKey,并忽略浏览器给出的值。

绘制与校正结构

要添加键,标注者先在左侧工具栏中选择键类型、环、元素或电荷,然后点击画布的空白处,或从某个原子拖出。把指针指向一个原子并键入元素符号(o、n、s)会改变该原子的元素。文本框接受 SMILES 字符串或 molfile,点击 Load(对 SMILES 字符串也可以按 Enter)会替换当前绘制的结构。OpenChemLib 无法读取的文本会被拒绝,当前结构保持不变。Clear 清空画布,Restore the given structure 恢复预填的结构。

编辑器下方的 SMILES、分子式和 InChIKey 在标注者绘制的过程中随之更新。化合价问题以“C (atom 2) has 5 bonds; it allows 4.”的形式列在那里;设置 require_valid 时,这些问题会阻止提交必填条目。

选择原子

标注者点击绘图中的原子来选中它,再次点击则取消选择。在原子列表中,Tab 移到列表,方向键、Home 和 End 在原子之间移动,Space 或 Enter 切换某个原子的选中状态。设置了 labels 时,标注者先选一个标签,列表中的每个原子都有一个标签菜单和一个 Remove 按钮。页面用元素和 molfile 编号命名原子,O4 就是源结构中的第四个原子。

存储的原子索引是该原子在所写源结构中从 0 开始的位置,按 SMILES 字符串中原子的顺序或 molfile 原子块中的顺序计算。molfile 原子编号等于索引加 1。无论由哪个引擎绘制分子,这个索引都等于 RDKit 以 removeHs=False 读取同一输入时的原子索引。OpenChemLib 会把 SMILES 字符串中的显式氢([H]C(Cl)(Br)F)并入相邻原子,因此在 engine: openchemlib 下,这样的 SMILES 字符串无法用于选择。显示器会给出提示,并且不提供任何可选原子。需要单独选择氢原子时,请使用 molfile 或 engine: rdkit。隐式氢(CCO)始终不可选择。

存储的答案

chemical_structure 方案为每个条目存储一个 JSON 对象:

json
{"molfile": "\nOCL MolfileCreator  2D\n\n  0  0  0 ... V3000\n...M  END\n",
 "smiles": "CC(Oc1ccccc1C(O)=O)=O",
 "idcode": "dklB@@QmR[fUxUZBBF@@",
 "formula": "C9H8O4",
 "rdkit_smiles": "CC(=O)Oc1ccccc1C(=O)O",
 "inchi": "InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)",
 "inchikey": "BSYNRYMUTXBXSQ-UHFFFAOYSA-N",
 "changed": true,
 "problems": [],
 "toolkits": {"openchemlib": "9.25.0", "rdkit": "2026.03.6"}}

molfile 为 V3000 格式。smiles 是 OpenChemLib 的规范异构 SMILES(isomeric SMILES),idcode 是 OpenChemLib 的规范 ID code。rdkit_smiles、inchi 和 inchikey 只在 RDKit.js 运行过时才出现。不同工具包生成的规范 SMILES 并不相同,因此应当用 InChIKey 或 ID code 比较结构,不要拿 smiles 和 rdkit_smiles 相比。只要所绘结构的 ID code 与预填结构相同,changed 就为 false。

molecule 显示类型上的 viewer_selection 方案存储选中的原子:

json
{"viewer": "molecule", "items": [{"atom": 12, "element": "N", "label": "wrong element"}]}

导出

chemistry 导出覆盖 chemical_structure 方案,以及绑定到 molecule 显示类型的 viewer_selection 方案:

bash
python -m potato.export --config config.yaml --format chemistry --output out/
文件内容
chemical_structures.csv每个答案一行,列为 user_id, instance_id, schema, smiles, rdkit_smiles, inchi, inchikey, idcode, formula, changed, problems, identifiers_from, answer_correct, answer_reason
chemical_structures.sdf每个答案的 molfile,该行的非空字段作为 SD 属性
molecule_atoms.csv每个选中的原子一行,列为 user_id, instance_id, schema, viewer_field, atom, number, element, label,其中 number 是 molfile 编号

RDKit.js 计算了 InChI 和 InChIKey 时,identifiers_from 为 browser;导出用 rdkit Python 包计算了两者时为 server;两者都不可用时为 none。导出会为没有标识符的答案列出警告。对设置了 answer_field 的方案,answer_correct 为 1 或 0,answer_reason 给出评分理由,例如“a different structure (ignoring stereo)”或“missing the substructure C(=O)[OH]”。RDKit 的 SDMolSupplier 和其他 SD 读取器打开这个 SD 文件时,会把这些字段读作属性。

Potato 的一致性指标不覆盖 chemical_structure 或 viewer_selection 方案,第一个配置中的 verdict 单选题照常计算一致性。要比较标注者给出的结构,可以按 instance_id 和 schema 匹配 chemical_structures.csv 中的行,再比较 inchikey;没有任何 RDKit 可用时比较 idcode。对于原子选择,从 molecule_atoms.csv 逐条目计算集合重叠,例如基于 atom 索引的 Jaccard 系数。

示例

bash
python potato/flask_server.py start examples/advanced/chemical-structure-correction/config.yaml -p 8000

这个示例包含五张常见药物的结构图像,每张图像附一个识别出的 SMILES 字符串。其中四个识别结果经过手工修改,带有典型的识别错误,例如杂原子错误或立体中心反转。同一目录下的 generate_images.py 根据真实的 SMILES 绘制这些图像。

用编程智能体搭建研究

potato-skill 1.2.0 面向 Potato 2.13.1,认得 chemical_structure 和 molecule 这两个名称,并指导研究设计这一侧,例如向标注者问什么、每个结构由几名标注者检查、培训和注意力检查,以及导出。这个技能目前还没有记录结构数据格式、标准答案模式、显示选项和 chemistry 可选依赖。安装 potato-skill,把研究描述给你的智能体,再让智能体参考本页的 Markdown 副本 https://www.potatoannotator.com/docs/annotation-types/chemical-structures.md,以了解格式和选项。

延伸阅读

有关实现细节,请参阅源文档。