跳转至

Building and testing

The gates

一条命令跑全部:

bash harness/gates.sh

顺序执行、任何一条非 0 就停:

cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --release
cargo test --workspace                     # debug,让 debug_assert 真的跑到
cargo doc --workspace --no-deps --document-private-items

# omgkit-conf 的三个外部判官,跑冒烟档(基准随仓库入库)
SMOKE=harness/baseline/smoke.bounds.jsonl
cargo run -p omgkit-conf --release --example smooth_oracle -- $SMOKE
cargo run -p omgkit-conf --release --example bounds_oracle -- $SMOKE
cargo run -p omgkit-conf --release --example eigen_oracle  -- $SMOKE \
    harness/baseline/smoke.gram_eigs.jsonl

(这一节的标题原先写着 "The five gates",而条数一路在涨 —— 数字写进标题就会 掉队。现在不写数字:要知道有几道,看 harness/gates.sh 里的 TOTAL。)

上面列的不是全部。 那几条是不需要 Python 环境就能跑的一档;gates.sh 里 还有几十条要 RDKit 的外部判官(以及一条跑文档里的示例)。别把这一小段当成 "CI 就这些"。

语料判据要随算法一起回来。 判据不进 CI,"违例不许涨"就只是文档里的一句话 —— 上一轮正是这么漏的:判据只进了本地脚本,推送时一条都不执行,而 CI 一直是绿的。

harness/gates.shset -eo pipefail,而且判据后面不许接管道 —— set -e 遇到管道只看最后一个命令的退出码,tail 永远成功,于是判据非 0 退出、 脚本照样打印"全部通过"。那是自己造的绿。要看少几行,把管道加在外面: bash harness/gates.sh 2>&1 | tail -40

harness/gates.sh runs all of them and more — it is the local mirror of CI, which stays the authority. Run it before opening a pull request.

cargo test --release rather than debug: the differential tests run over a large corpus and a debug build is too slow to be useful.

cargo test --workspace on top of it, in debug, for one reason: debug_assert! is compiled out of a release build. The workspace has 38 of them — "bond geometry was written but cis/trans was never perceived", "canonical ranks are not injective", "a flip reached into another fragment" — and with only the release run in CI, not one of them had ever been executed by a gate. A guard that never runs is a guard that is not there. The debug run takes about 6 seconds for the whole workspace, so it buys that back cheaply.

Why omgkit-py is not in the default workspace members

default-members = [
    "crates/omgkit-core",
    "crates/omgkit-io",
    "crates/omgkit-chem",
    "crates/omgkit-match",
    "crates/omgkit-depict",
    "crates/omgkit-conf",
]

omgkit-py is a cdylib that leaves Python symbols unresolved at link time. Building a test executable for it necessarily fails, so putting it in the default set would turn every project-wide verification command red. It stays in members so it shares the lockfile and target directory and cargo -p can reach it.

Do not add panic = "abort"

The Python extension relies on unwinding to catch Rust panics and turn them into Python exceptions. With abort, a panic sends SIGABRT to the interpreter — exit code 134, no exception, no traceback, nothing try/except can catch, and the user loses their whole process along with any unsaved work.

Cargo does not allow overriding panic per package, so this is a workspace-wide trade. The cost is the unwind tables and a small constant overhead, which does not show up on the pipeline benchmark.

The Python extension

pip install maturin
maturin build --release -m crates/omgkit-py/Cargo.toml --out dist
pip install --force-reinstall dist/omgkit-*.whl
python harness/test_python.py

Differential tests

The tests come in two tiers.

Smoke tier — oracles are committed (about 2.2 MB), runs by default, green on a fresh clone.

Large-corpus tier — marked #[ignore], needs oracles you generate against an external reference implementation:

cargo test --release -- --ignored

Generating the oracles, the column conventions for each layer, and what each judge is guarding are documented in harness/README.md — also readable here as the full text (Chinese).

The gitignore rule for oracles is about tests, not filenames

Baselines that a non-#[ignore] test reaches must be committed, or a fresh clone fails immediately. Guessing by filename has gone wrong twice: once a rule written as smoke.*.jsonl excluded smoke.matches.tsv by suffix, once smarts.jsonl was missed because it is not named smoke but a non-ignored test hard-codes it.

The way to verify a change to that rule is not to read it — it is to clone the repository somewhere else and run cargo test.

omgkit-depict has a third tier of its own — the properties a picture must satisfy, run over the whole corpus rather than a hand-picked list:

# eight decidable properties over 8831 molecules × 2 styles
cargo run -p omgkit-depict --release --example audit -- harness/corpus/large.smi

# eyeball it: 17 molecules × 2 styles × svg/png/jpg
cargo run -p omgkit-depict --release --features raster --example draw -- out/

# side by side with RDKit at the same bond length, then bind the lot
# into one PDF (byte-identical across runs; refuses to run if the manifest
# and the images on disk disagree in either direction)
python3 harness/compare_rdkit.py out/
python3 harness/make_gallery.py out/

Run the audit twice

One class of defect only shows up across processes: HashMap iteration order is seeded per run, so anything that sums positions in that order can silently give a different picture each time. Two runs that disagree is the only way to see it — no unit test can, because the seed is fixed within a process.

The raster feature is optional on purpose: without it the crate has no external dependencies and emits SVG only.

初始构型:确定性距离几何(建设中)

omgkit-conf 的 v1 与 v2 都撤了。两版栽在同一件事上:按分子的类别切分支 —— 无环走构造法、有环另说、超配位拒绝、累积双键再加一条。每来一类分子就多一个分支, 覆盖率停在 14.3%(1259 / 8831),而 RDKit ETKDG 是 99.48%(2026-08-26 拿钉住的 2025.09.2 重量是 99.59%,见 harness/baseline_rdkit_etkdg.py)。 旧代码在 git 历史里(4eefccd 及之前),需要参考时 git show 取。

现在这一版走的是距离几何,与 RDKit 同一条主干,只在一处分岔: RDKit 在界矩阵里逐对独立随机取一组距离,取出来的表常常任何空间都摆不出来, 它的应对是作废整次尝试重掷;这里直接拿三角光滑化之后的上限矩阵 U 当参考距离表 —— U 按构造满足三角不等式,而且全程没有随机数

已落地的分块,每块都配了外部判官(判官不进 CI 就不是闸,所以三条都在上面的闸门里):

分块 判官 现状
三角光滑化 RDKit 的 GetMoleculeBoundsMatrix 带/不带 smoothing 逐位相同,最大偏差 5.3e-15
界矩阵 真实构象要落在界内 + 界宽不许比 RDKit 松 + U 要摆得进三维 越界 0.863%;宽度比 1.004;1-2/1-3/1-4 三档与 RDKit 逐位相同
全语料硬不变量 8831 个分子跑完整条流水线:界可行率、原子完全重合、坐标非有限数 界不可行 0.01%(1 个;同一份语料上 RDKit ETKDGv3 2025.09.2 是 0.41%,36 个);重合 0;非有限 0
手性中心 真值 = 真实构象上量出来的有符号体积 247 个中心,符号错 0、漏抽 0
端到端(产物) 精修前后各量一遍:越界、自穿、手性、耗时 1-2/1-3/1-4/长程 越界 0.0 / 0.1 / 0.0 / 0.0%;键交叉 1731 → 0;手性 85.1% → 100%;1.45 ms/分子(全语料口径 1.1 ms,参照 6.1 ms)
自穿 先拿真实构象校准检测器(必须报 0),再量自己 真实构象 0 误报;我们嵌出来的环穿刺 1/400(0.2%),键交叉 1821 —— 后者正是精修要收拾的
通用性难例语料 68 个分子,照着算法的假设挑:笼状/张力、超配位、累积双键、超大环、少见元素、金属、自由基、两性离子 建界即空 0、界不可行 0这一档 RDKit 追上来了:2026-08-26 拿钉住的 2025.09.2 重量,同一批分子它失败 0 个 —— 先前记的"失败 2 个(SF₆ 与六氨合钴)"出自更早的版本
特征分解 + 嵌入 numpy eigvalsh(LAPACK)+ 真实构象精确回嵌 特征值偏差 5.96e-15;回嵌偏差 1.76e-11 Å

整条流水线已经通了:界矩阵 → 三角光滑化 → 取 U 当参考距离表 → 度量矩阵嵌入 → 全局手性定向(离散一次)→ L-BFGS 精修,全程无随机数。

还没做:确定性的重试阶梯、1-5 链式约束。已知的一笔债:饱和环的 1-4 扭转退回了全程(界宽比 7.68×),换来的是约束自洽 —— 正确修法是把 ring_internal_torsion 的分桶扩到"是否全 sp³"并用与键角自洽的值,要重跑参数表实测。

整体手性不能指望三维精修去修:翻转手性是反射(det = −1),不在 SO(3) 的 连通分支里,连续下降要走到镜像必须把分子压平,下降法不会付这个势垒。所以嵌入之后 离散地定一次全局定向 —— 这一步已经落地。实测(247 个中心):

手性号正确的中心
嵌完直接看 53.0%(基本是掷硬币,与"定向任意"吻合)
做一次全局反射后 86.2%

剩下的 13.8% 是个别中心相对多数错,全局反射按定义救不了,但三维精修救得了 —— 翻一个中心只要它自己的体积过零(局部、有限势垒),而全局反射要求所有中心同时压平。 所以四维先不做,等精修落地再量剩下多少。(RDKit 一有手性中心就上四维, 为的正是让手性能连续翻:四维里 (x₃, x₄) 平面转 π 就把 x₃ 送到 −x₃, 而四维两两距离精确不变。)

"通用"的判定标准不是"在语料上都过",是"来了一类没见过的分子,要改的是不是只有 约束表"。 代码里出现 is_metal / is_macrocycle 这类分子类别谓词就算违规 —— 前两版 omgkit-conf 正是死在这里。难例语料(harness/corpus/hard.smi)是这条标准 的探针:每一类精确攻击一条假设,红了就说明那条假设不成立,而修法只许是补表。

harness/params/ 里的实测参数表与 harness/baseline_rdkit_etkdg.py 是从语料量出来的 数据与基线,与算法怎么写无关,一直留着。

Documentation

cargo doc --workspace --no-deps --open     # Rust API

pip install -r docs/requirements.txt        # the site
mkdocs serve                                # http://127.0.0.1:8000