phm-molcheck 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
phm_molcheck/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""phm-molcheck public API.
|
|
3
|
+
|
|
4
|
+
Deterministic physical-validity checks for AI-generated molecular /
|
|
5
|
+
protein structures (PDB-level):
|
|
6
|
+
parse_pdb_atoms(path) -- PDB parser (filters crystal waters, altloc B+, hydrogens)
|
|
7
|
+
grid_clash(atoms) -- C1 clash detection (MolProbity-style, grid-accelerated)
|
|
8
|
+
bond_check(atoms) -- C2 bond-length sanity
|
|
9
|
+
qc_structure(path, ref) -- single-structure verdict (C1/C2/C4, +C3 with ref)
|
|
10
|
+
batch_qc(pattern, ...) -- batch report with SHA-256 audit digests
|
|
11
|
+
VDW, COV, BOND_TOL, CLASH_FACTOR, GRID
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .checker import (parse_pdb_atoms, grid_clash, bond_check,
|
|
15
|
+
qc_structure, batch_qc,
|
|
16
|
+
VDW, COV, BOND_TOL, CLASH_FACTOR, GRID)
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
__all__ = ["parse_pdb_atoms", "grid_clash", "bond_check", "qc_structure",
|
|
20
|
+
"batch_qc", "VDW", "COV", "BOND_TOL", "CLASH_FACTOR", "GRID",
|
|
21
|
+
"__version__"]
|
phm_molcheck/checker.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
phm-molcheck -- deterministic physical-validity gate for AI-generated
|
|
4
|
+
molecular / protein structures (PDB-level, no RDKit required).
|
|
5
|
+
|
|
6
|
+
Architecture principle: the checker does not trust the generating model's
|
|
7
|
+
internal state; it reads only the output structure file (.pdb/.cif).
|
|
8
|
+
Checks (deterministic physics/chemistry rules, no learnable parameters):
|
|
9
|
+
C1 atomic clash detection (MolProbity-style, grid-accelerated):
|
|
10
|
+
non-bonded pair with overlap = r_vdw_i + r_vdw_j - d >= 0.4 A
|
|
11
|
+
C2 bond-length sanity: covalent distance vs standard coval radii,
|
|
12
|
+
deviation > 25% flagged
|
|
13
|
+
C3 cross clashes between a generated binder and a reference
|
|
14
|
+
structure (ligand/target), when a reference is supplied
|
|
15
|
+
C4 packing density observation (atoms per 1000 A^3)
|
|
16
|
+
Output: per-structure PASS/FAIL verdict + batch summary + SHA-256 audit
|
|
17
|
+
digest of every input file.
|
|
18
|
+
|
|
19
|
+
Corigin note: battle-tested as an independent QC layer on generated
|
|
20
|
+
protein-binder structures; generalized here as a model-agnostic tool.
|
|
21
|
+
"""
|
|
22
|
+
import sys, os, json, glob, hashlib, time
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
VDW = {'C': 1.70, 'N': 1.55, 'O': 1.52, 'S': 1.80, 'P': 1.80, 'H': 1.20, 'F': 1.47, 'CL': 1.75, 'BR': 1.85, 'ZN': 1.39, 'MG': 1.73, 'CA': 2.31, 'FE': 1.94, 'SE': 1.90, 'I': 1.98}
|
|
26
|
+
COV = {'C': 0.76, 'N': 0.71, 'O': 0.66, 'S': 1.05, 'P': 1.07, 'H': 0.31, 'F': 0.57, 'CL': 1.02, 'BR': 1.20, 'ZN': 1.22, 'MG': 1.41, 'CA': 1.76, 'FE': 1.32, 'SE': 1.20, 'I': 1.39}
|
|
27
|
+
BOND_TOL = 0.25 # 键长偏差容忍25%
|
|
28
|
+
CLASH_FACTOR = 0.75
|
|
29
|
+
GRID = 4.0 # 碰撞检测网格(Å)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_pdb_atoms(path):
|
|
33
|
+
"""解析PDB: 返回 [(elem, x, y, z, chain, residx, hetero)]
|
|
34
|
+
过滤: 结晶水(HOH)与altloc双构象(只留A/空白)——晶体学伪clash源"""
|
|
35
|
+
atoms = []
|
|
36
|
+
with open(path) as f:
|
|
37
|
+
for line in f:
|
|
38
|
+
if line.startswith(('ATOM ', 'HETATM')):
|
|
39
|
+
resn = line[17:20].strip()
|
|
40
|
+
if resn in ('HOH', 'H2O', 'WAT'):
|
|
41
|
+
continue # 结晶水: 位置受晶体学约束, 非碰撞评估对象
|
|
42
|
+
altloc = line[16:17].strip()
|
|
43
|
+
if altloc and altloc != 'A':
|
|
44
|
+
continue # 替代构象: 双构象原子互相重叠, 只留A
|
|
45
|
+
elem = line[76:78].strip().upper() if len(line) >= 78 else line[12:16].strip().strip('0123456789').upper()
|
|
46
|
+
if elem.startswith('H'):
|
|
47
|
+
continue # 氢原子: PDB晶体结构常不给氢或给不全, 跳过
|
|
48
|
+
try:
|
|
49
|
+
x, y, z = float(line[30:38]), float(line[38:46]), float(line[46:54])
|
|
50
|
+
except ValueError:
|
|
51
|
+
continue
|
|
52
|
+
chain = line[21:22].strip()
|
|
53
|
+
het = line.startswith('HETATM')
|
|
54
|
+
atoms.append((elem if elem in VDW else 'C', x, y, z, chain, line[22:26], het))
|
|
55
|
+
return atoms
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ridx(s):
|
|
59
|
+
"""残基号规范化: ' 123A'->123 ( Chothia/Kabat插入码取数值部分)"""
|
|
60
|
+
try:
|
|
61
|
+
return int(''.join(c for c in s if c.isdigit()) or s)
|
|
62
|
+
except (ValueError, TypeError):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def grid_clash(atoms, overlap_tol=0.4, exclude_same_residue=True):
|
|
67
|
+
"""网格加速碰撞检测(MolProbity标准): 重叠=r_i+r_j-d >= 0.4Å => clash
|
|
68
|
+
排除规则: 同残基内全部原子对 + 同链相邻残基(数值号差1)肽键C-N"""
|
|
69
|
+
if len(atoms) < 2:
|
|
70
|
+
return []
|
|
71
|
+
xyz = np.array([(a[1], a[2], a[3]) for a in atoms])
|
|
72
|
+
cells = {}
|
|
73
|
+
for i, (x, y, z) in enumerate(xyz):
|
|
74
|
+
cells.setdefault((int(x // GRID), int(y // GRID), int(z // GRID)), []).append(i)
|
|
75
|
+
clashes = []
|
|
76
|
+
offsets = [(dx, dy, dz) for dx in (-1, 0, 1) for dy in (-1, 0, 1) for dz in (-1, 0, 1)]
|
|
77
|
+
for key, idxs in cells.items():
|
|
78
|
+
for off in offsets:
|
|
79
|
+
nkey = (key[0] + off[0], key[1] + off[1], key[2] + off[2])
|
|
80
|
+
if nkey not in cells:
|
|
81
|
+
continue
|
|
82
|
+
for i in idxs:
|
|
83
|
+
ei, xi = atoms[i][0], xyz[i]
|
|
84
|
+
for j in cells[nkey]:
|
|
85
|
+
if j <= i:
|
|
86
|
+
continue
|
|
87
|
+
ej, xj = atoms[j][0], xyz[j]
|
|
88
|
+
# 排除1: 同一残基内原子对(共价几何, 非碰撞)
|
|
89
|
+
if exclude_same_residue and atoms[i][4] == atoms[j][4] and atoms[i][5] == atoms[j][5]:
|
|
90
|
+
continue
|
|
91
|
+
# 排除2: 同链相邻残基(插入码已规范化)间全部原子对——
|
|
92
|
+
# 肽键C-N 1.33/O-N 2.25/CA-N 2.4等均为构象几何固定值, 非碰撞
|
|
93
|
+
ri, rj = _ridx(atoms[i][5]), _ridx(atoms[j][5])
|
|
94
|
+
if (ri is not None and rj is not None and abs(ri - rj) == 1
|
|
95
|
+
and atoms[i][4] == atoms[j][4]):
|
|
96
|
+
continue
|
|
97
|
+
d = float(np.linalg.norm(xi - xj))
|
|
98
|
+
if d < 1e-3:
|
|
99
|
+
continue # 同一原子
|
|
100
|
+
# 二硫键白名单: CYS-SG ~ S-S 2.05Å共价键
|
|
101
|
+
if {atoms[i][0], atoms[j][0]} == {'S'} and 1.8 <= d <= 2.3:
|
|
102
|
+
continue
|
|
103
|
+
overlap = VDW[ei] + VDW[ej] - d
|
|
104
|
+
if overlap >= overlap_tol:
|
|
105
|
+
clashes.append((i, j, round(d, 3), round(overlap, 2)))
|
|
106
|
+
return clashes
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def bond_check(atoms):
|
|
110
|
+
"""键长合理性: 对距离在[0.9,1.9]x共价和的原子对, 检查偏差"""
|
|
111
|
+
issues = []
|
|
112
|
+
xyz = np.array([(a[1], a[2], a[3]) for a in atoms])
|
|
113
|
+
n = len(atoms)
|
|
114
|
+
for i in range(n):
|
|
115
|
+
ei, xi = atoms[i][0], xyz[i]
|
|
116
|
+
for j in range(i + 1, min(i + 40, n)):
|
|
117
|
+
ej, xj = atoms[j][0], xyz[j]
|
|
118
|
+
d = float(np.linalg.norm(xi - xj))
|
|
119
|
+
if d > 2.2:
|
|
120
|
+
continue
|
|
121
|
+
ideal = COV[ei] + COV[ej]
|
|
122
|
+
# 氢键/盐桥范围排除(2.5-3.5Å不算共价)
|
|
123
|
+
if abs(d - ideal) / ideal > BOND_TOL and d < ideal * 1.25 - 0.35:
|
|
124
|
+
issues.append((i, j, round(d, 3), round(ideal, 3)))
|
|
125
|
+
return issues
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def qc_structure(pdb_path, ref_pdb=None):
|
|
129
|
+
"""对单个生成结构跑C1/C2/C4, ref_pdb存在则加跑C3"""
|
|
130
|
+
t0 = time.time()
|
|
131
|
+
atoms = parse_pdb_atoms(pdb_path)
|
|
132
|
+
r = {'file': os.path.basename(pdb_path), 'n_atoms': len(atoms)}
|
|
133
|
+
if not atoms:
|
|
134
|
+
r['verdict'] = 'NO_ATOMS'
|
|
135
|
+
return r
|
|
136
|
+
clashes = grid_clash(atoms)
|
|
137
|
+
r['C1_clash_pairs'] = len(clashes)
|
|
138
|
+
r['C1_examples'] = [(atoms[i][0], atoms[j][0], d, ov) for i, j, d, ov in clashes[:5]]
|
|
139
|
+
bonds = bond_check(atoms)
|
|
140
|
+
r['C2_bad_bonds'] = len(bonds)
|
|
141
|
+
if ref_pdb and os.path.exists(ref_pdb):
|
|
142
|
+
ref_atoms = parse_pdb_atoms(ref_pdb)
|
|
143
|
+
both = atoms + ref_atoms
|
|
144
|
+
cross = [(i, j, d) for i, j, d, ov in grid_clash(both) if (i < len(atoms)) != (j < len(atoms))]
|
|
145
|
+
r['C3_cross_clash_with_ref'] = len(cross)
|
|
146
|
+
xyz = np.array([(a[1], a[2], a[3]) for a in atoms])
|
|
147
|
+
vol = float(np.prod(xyz.max(0) - xyz.min(0) + 1e-6))
|
|
148
|
+
r['C4_atoms_per_1000A3'] = round(len(atoms) / max(vol, 1) * 1000, 2) if vol > 0 else 0
|
|
149
|
+
r['verdict'] = 'FAIL' if (r['C1_clash_pairs'] > 0 or r.get('C3_cross_clash_with_ref', 0) > 0) else 'PASS'
|
|
150
|
+
r['audit_sha256'] = hashlib.sha256(open(pdb_path, 'rb').read()).hexdigest()[:16]
|
|
151
|
+
r['elapsed_s'] = round(time.time() - t0, 3)
|
|
152
|
+
return r
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def batch_qc(pattern, ref_pdb=None, out_json=None):
|
|
156
|
+
files = sorted(glob.glob(pattern))
|
|
157
|
+
report = {'tool': 'phm-molcheck (deterministic physical validity gate)',
|
|
158
|
+
'n_files': len(files), 'results': [], 't': time.strftime('%F %T')}
|
|
159
|
+
for f in files:
|
|
160
|
+
report['results'].append(qc_structure(f, ref_pdb))
|
|
161
|
+
n_pass = sum(1 for x in report['results'] if x.get('verdict') == 'PASS')
|
|
162
|
+
n_fail = sum(1 for x in report['results'] if x.get('verdict') == 'FAIL')
|
|
163
|
+
report['summary'] = {'PASS': n_pass, 'FAIL': n_fail,
|
|
164
|
+
'pass_rate': round(n_pass / max(len(files), 1), 4),
|
|
165
|
+
'total_clash_pairs': sum(x.get('C1_clash_pairs', 0) for x in report['results'])}
|
|
166
|
+
blob = json.dumps(report, ensure_ascii=False, default=str)
|
|
167
|
+
report['batch_audit_sha256'] = hashlib.sha256(blob.encode()).hexdigest()[:16]
|
|
168
|
+
if out_json:
|
|
169
|
+
json.dump(report, open(out_json, 'w'), ensure_ascii=False, indent=1)
|
|
170
|
+
return report
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == '__main__':
|
|
174
|
+
pattern = sys.argv[1] if len(sys.argv) > 1 else './demo/**/*.pdb'
|
|
175
|
+
ref = sys.argv[2] if len(sys.argv) > 2 else None
|
|
176
|
+
out = sys.argv[3] if len(sys.argv) > 3 else './phm_molcheck_report.json'
|
|
177
|
+
rep = batch_qc(pattern, ref, out)
|
|
178
|
+
print(json.dumps(rep['summary'], ensure_ascii=False, indent=1))
|
|
179
|
+
print('batch audit digest:', rep['batch_audit_sha256'])
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phm-molcheck
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Deterministic physical-validity gate for AI-generated molecular/protein structures (PDB-level, no RDKit required).
|
|
5
|
+
Author: PHM-Audit
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PHM-Audit/phm-molcheck
|
|
8
|
+
Keywords: ai-generated,molecules,proteins,pdb,clash,validation,drug-discovery,structure-quality,deterministic
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy>=1.20
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# phm-molcheck
|
|
22
|
+
|
|
23
|
+
Deterministic **physical-validity gate for AI-generated molecular and
|
|
24
|
+
protein structures** (PDB-level). Pure checks, no learnable parameters,
|
|
25
|
+
no RDKit dependency — just `numpy`.
|
|
26
|
+
|
|
27
|
+
The checker does not trust the generating model's internal state; it
|
|
28
|
+
reads only the output structure file and answers one question: *is this
|
|
29
|
+
structure physically plausible?*
|
|
30
|
+
|
|
31
|
+
Battle-tested as an independent QC layer on AI-generated protein-binder
|
|
32
|
+
structures; generalized here as a model-agnostic tool.
|
|
33
|
+
|
|
34
|
+
## Checks (all deterministic)
|
|
35
|
+
|
|
36
|
+
| ID | Check | Rule |
|
|
37
|
+
|----|-------|------|
|
|
38
|
+
| C1 | atomic clashes | non-bonded pair with overlap `r_vdw_i + r_vdw_j - d >= 0.4 A` (MolProbity-style, grid-accelerated) |
|
|
39
|
+
| C2 | bond-length sanity | covalent distance vs standard covalent radii, deviation > 25% flagged |
|
|
40
|
+
| C3 | cross clashes | generated structure vs a reference (ligand/target), when supplied |
|
|
41
|
+
| C4 | packing density | atoms per 1000 A^3 (observational) |
|
|
42
|
+
|
|
43
|
+
Parser hygiene: crystal waters (HOH/WAT), altloc B+ conformations, and
|
|
44
|
+
hydrogens are filtered — the classic false-clash sources in PDB files.
|
|
45
|
+
|
|
46
|
+
Every input file gets a SHA-256 digest; every batch report carries an
|
|
47
|
+
audit digest. Same input → same report, always.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install numpy
|
|
53
|
+
pip install git+https://github.com/PHM-Audit/phm-molcheck
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Use as a library
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from phm_molcheck import qc_structure, batch_qc
|
|
60
|
+
|
|
61
|
+
r = qc_structure("generated_binder.pdb", ref_pdb="target_complex.pdb")
|
|
62
|
+
print(r["verdict"]) # PASS / FAIL / NO_ATOMS
|
|
63
|
+
print(r["C1_clash_pairs"]) # number of severe non-bonded overlaps
|
|
64
|
+
print(r["C2_bad_bonds"]) # unreasonable covalent distances
|
|
65
|
+
|
|
66
|
+
rep = batch_qc("./out/**/*.pdb", ref_pdb="target_complex.pdb",
|
|
67
|
+
out_json="report.json")
|
|
68
|
+
print(rep["summary"]) # {'PASS': .., 'FAIL': .., 'pass_rate': ..}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Use as a CLI
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python -m phm_molcheck.checker "./generated/*.pdb" target.pdb report.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Scope and limits
|
|
78
|
+
|
|
79
|
+
- This package contains deterministic PDB-level geometric checks only.
|
|
80
|
+
It does not perform force-field minimization, learned quality
|
|
81
|
+
prediction, or chemistry-aware graph validation (that would require
|
|
82
|
+
RDKit or heavier machinery).
|
|
83
|
+
- Verdicts are mechanical: C1/C3 clashes drive FAIL; C4 is observational.
|
|
84
|
+
- Hydrogens are ignored (crystal structures typically omit them).
|
|
85
|
+
|
|
86
|
+
## Tests
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install pytest numpy
|
|
90
|
+
pytest tests/ -q
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
phm_molcheck/__init__.py,sha256=4XA4NhiB7cTLFKQGMEBYxGj7oGSTVGbblxDNxdK7gCs,949
|
|
2
|
+
phm_molcheck/checker.py,sha256=kw3MgB5uAdz40ZUQcxWqlBeq9hDd2_o4-fqqhzg4srs,8626
|
|
3
|
+
phm_molcheck-1.0.0.dist-info/licenses/LICENSE,sha256=zbKC6uZ8MmD8rc_ds57f1g0TCkdlXaOJoHZAM1tc8G8,1085
|
|
4
|
+
phm_molcheck-1.0.0.dist-info/METADATA,sha256=W9H2o9sAxbedeWD-egsIi1KO1LsCQ7u12kNq8s9-2I4,3258
|
|
5
|
+
phm_molcheck-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
phm_molcheck-1.0.0.dist-info/top_level.txt,sha256=7GeYTvaqUCTIQ8qS1dE3bmFUAJQ0-_dcc_qf73WgefU,13
|
|
7
|
+
phm_molcheck-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PHM Audit Cloud contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
phm_molcheck
|