xyzsdf
按二维结构图给三维分子定取向,并把同一个取向复用到该次计算的所有文件上。
解决什么问题
量化程序(Gaussian、ORCA…)输出的几何用的是它自己的标准取向——由惯性主轴决定,和化学家怎么看这个分子毫无关系。于是同一批分子渲染出来,每个朝向都不一样:这个侧着、那个背对着、下一个躺平。作为一套图,读者没法比较。
而 ChemDraw 画的二维结构图恰恰是人挑好的视角。它已经决定了"这个分子应该这么看"。
这个包做的就是把后者的取向搬到前者上:求出让三维结构正对二维画法的那个旋转。
它和查看器自带的"自动定向"(主轴、--orient 之类)不是一回事——自动定向只能猜一个几何上合理的角度,猜不出你想让读者看哪一面。二维结构图携带的正是这个信息。
from xyzsdf import align_structures, formats
alignment = align_structures(open("mol.xyz").read(), open("ref.sdf").read(), "ref.sdf")
formats.apply_transform(open("homo.cube").read(), alignment.transform, "cube") # 转 cube
alignment.transform.as_matrix4() # 或者当相机用
一次计算里的 xyz、cube 本来就在同一个坐标系里,所以一个变换覆盖它名下所有文件。
核心是变换,不是文件
对齐的产物是一个 RigidTransform,不是一个转好的文件。
这不是措辞讲究,而是因为不同格式"施加旋转"的代价差好几个数量级:
| 文件 | 施加旋转的代价 |
|---|---|
| XYZ | 坐标乘矩阵 |
| cube | 改 header 里的 origin 和三个网格轴,体数据一字节不动 |
| molden | 坐标 加上 全部 MO 系数(按角动量做 Wigner-D 旋转) |
| 查看器 | 把 4×4 矩阵当相机用,什么文件都不用改 |
所以核心算完变换就停手,怎么花由调用方决定。要是你的前端能接受相机矩阵,最省事的做法是文件一个都别动——同一次计算的 xyz 和 cube 本来就在同一个坐标系里,转一次相机全都对。
安装
pip install xyzsdf # 核心:numpy + rdkit
pip install 'xyzsdf[db]' # + psycopg2,批量驱动
pip install 'xyzsdf[render]' # + xyzrender,出图
pip install -e . # 从源码开发安装
需要 Python 3.10+。rdkit>=2022.09 不是随手写的下限——键感知用的
rdDetermineBonds 正是那一版才引入。
命令行
# 对齐;--apply-to 在同一个进程里把变换施加到别的文件,不产生中间文件
xyzsdf-align mol.xyz ref.sdf -o out.xyz --apply-to homo.cube lumo.cube
# 只有需要跨进程/跨机器传递变换时才导出 JSON
xyzsdf-align mol.xyz ref.sdf --dump-transform t.json
xyzsdf-apply t.json homo.cube
# 批量:从 PostgreSQL 读结构,默认 dry-run(表结构见下方说明)
xyzsdf-align-db --host H --port 5432 --user U --password P \
--dbname D --verbose --error-file errors.txt [--apply]
# 出图(需要 xyzrender)
./extras/render_mo.sh homo_aligned.cube out.png [iso] [opacity]
源码树里 python3 align_xyz_to_sdf.py … 等老写法仍然可用,根目录留了同名薄壳。
xyzsdf-align-db对表结构只有一个很弱的假设:两个存文本的列(3D 结构和 2D 描绘) 加一个可空的标记列,只处理标记列为 NULL 的行。表名和四个列名都是命令行参数:--table(必填)、--id-column、--xyz-column、--sdf-column、--pending-column。 它是从一个内部批处理脚本长出来的,把它当成"怎么接数据库"的可用示例即可——批处理的 核心不过是循环里那一句align_structures。
库
from pathlib import Path
from xyzsdf import align_structures, formats
xyz_text = Path("mol.xyz").read_text()
alignment = align_structures(xyz_text, Path("ref.sdf").read_text(), "ref.sdf")
print(f"2D RMS = {alignment.xy_rms:.4f}")
if alignment.is_degenerate:
print(f"取向存疑:次优视角相差 {alignment.rival_angle_deg:.0f}°")
rotated = formats.apply_transform(xyz_text, alignment.transform, "xyz")
camera = alignment.transform.as_matrix4() # 列向量约定,直接喂查看器
核心不做任何文件 IO:字符串进、字符串出,对象进、对象出。一个 alignment.transform
可以施加到任意多个文件,全程留在内存里。to_dict() 返回的是普通字典——落到 JSON、
数据库列、HTTP 响应,还是不落,由调用方决定。
读几何:
geom = formats.parse(text, "cube") # 也支持 xyz / sdf
len(geom), len(geom.heavy())
formats.supported_formats("read") # ['cub','cube','mol','sdf','xyz']
formats.supported_formats("apply") # ['cub','cube','xyz']
包结构
errors.py 失败分类
units.py 内部统一 Å,各格式在自己边界换算
structure/ 一个分子是什么,以及怎么再认出它
geometry.py Geometry —— "有哪些原子、在哪"的唯一真相
fingerprint.py 指纹,用于校验变换是否属于这个文件
transform/ 旋转:怎么表示、怎么拟合
rigid.py RigidTransform
fit.py Kabsch
formats/ 文本 <-> Geometry,以及施加变换
─────────── 以上只需 numpy,以下需要 rdkit ───────────
align/ 判断什么对应什么
bonding.py RDKit 唯一被问的问题:哪些原子成键
correspondence.py 哪个原子对哪个,以及有多确定
pipeline.py 编排
那条横线是有约束力的,不是装饰。施加一个存好的变换是纯算术,只想花变换的下游服务不该被逼着装一整套化学工具链;判断原子对应关系是图问题,那才真需要。tests/test_boundary.py 双向守住这条线。
Geometry 是本包的脊梁:每种格式都解析成它,所有消费者都从它读原子,RDKit 只被问一个问题——哪些原子成键。早期版本每个分子被解析两次(自己的 parser 一次、RDKit 一次)再对账,显式氢那个崩溃就住在对账的缝里。
几个踩过的坑
每条的详细复盘(症状、证据、根因、现在的防线)在 docs/pitfalls/。
| 坑 | 一句话 |
|---|---|
| 单一真相 | 同一份数据解析两遍,迟早分叉——显式氢崩溃的根因 |
| 取向简并 | 二维投影几乎分不清正反面,取向可能是猜的;看 is_degenerate |
| 点 vs 方向 | apply_points 和 apply_vectors 不能混用,混了会静默错位 |
| 约定与量纲 | 行/列向量差一个转置;pivot 有量纲,旋转矩阵没有 |
| 手性 | det(R) 掉到 −1 就把分子换成了对映体 |
| cube 格式 | 两个符号位、DSET_IDS 折行、转完不再轴对齐 |
| molden | 只转核不转 MO 系数 = 物理上错误的轨道,且不报错 |
| 来源校验 | 套错文件或施加两次,产物照样正常 |
| xyzrender | --no-orient 不能省;默认 --iso 0.05 藏掉整个轨道 |
| 工具链 | zsh 不做词分割、Python 3.12 移除 find_module 等 |
版本与稳定性
0.x 期间公开 API 可能变更,变更记录在 CHANGELOG.md。
测试
pytest # 或逐个:for t in tests/test_*.py; do python3 "$t"; done
CI 在 Linux 上跑 Python 3.10–3.13、外加一个 macOS,所以 requires-python
是跑出来的事实而不是声明。另外还会构建 wheel、用 twine check --strict
验证 PyPI 元数据、装进干净 venv 跑三个命令,并单独确认"只装 numpy 也能施加变换"。
| 文件 | 守住什么 |
|---|---|
test_boundary.py |
依赖分层双向成立;核心不在模块级 import 任何 extra |
test_geometry.py |
Geometry、格式能力声明、错误分类 |
test_alignment.py |
输出逐字节稳定、枚举顺序无关、det=+1、margin 区分明确与简并 |
test_provenance.py |
套错分子抛错、重复施加告警 |
测试数据是两个公开分子,由 tests/fixtures/generate.py 从 SMILES 可复现地生成
(ETKDG 固定种子 + MMFF)。aspirin_aligned.xyz 是逐字节基线,钉住整条流水线。
两个 fixture 刻意卡在取向确信度的两端:
| 分子 | 重原子 | 候选映射 | margin | 竞争视角 |
|---|---|---|---|---|
| 阿司匹林 | 13 | 2 | 0.92 | 3.0° |
| 四苯乙烯 | 26 | 128 | 0 | 180.0° |
四苯乙烯是螺旋桨形、准对称的分子,次优视角就是它的另一面、分数完全相同——正是
is_degenerate 要拦的那种情况。
许可证
Metadata
Release files for xyzsdf 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xyzsdf-0.1.0.tar.gz | 69.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xyzsdf-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 106.3 kB
Release files / xyzsdf-0.1.0.tar.gz
| Download URL | xyzsdf-0.1.0.tar.gz |
|---|---|
| Size | 69.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2e7ee4be87e25da83f45c22176ddd19a74220d00acc2a665474caca647b608bb
|
|
BLAKE2b-256 checksum How to use checksums |
c6724187281ae089efc896bea22e6714460275475f7cbefdecc34f8a0eabd6bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.
Transparency logRelease files / xyzsdf-0.1.0-py3-none-any.whl
| Download URL | xyzsdf-0.1.0-py3-none-any.whl |
|---|---|
| Size | 36.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f535efba81af8d335008ce0650d523f9d9540ac33aaa33d16556154a37631237
|
|
BLAKE2b-256 checksum How to use checksums |
d744ab18a7248e9a9ea032557523f5f1e20b71fb1697cb1be326da93a7e3209c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.
Transparency log