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.
@@ -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__"]
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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