vimol 0.1.3__tar.gz → 0.1.6__tar.gz
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.
- vimol-0.1.6/PKG-INFO +142 -0
- vimol-0.1.6/README.md +106 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/__init__.py +1 -1
- vimol-0.1.6/src/vimol/cistrans.py +266 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/select.py +111 -53
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/viewer.py +378 -110
- vimol-0.1.6/src/vimol.egg-info/PKG-INFO +142 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol.egg-info/SOURCES.txt +2 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_align.py +43 -0
- vimol-0.1.6/tests/test_cistrans.py +281 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_vimol.py +505 -115
- vimol-0.1.3/PKG-INFO +0 -199
- vimol-0.1.3/README.md +0 -163
- vimol-0.1.3/src/vimol.egg-info/PKG-INFO +0 -199
- {vimol-0.1.3 → vimol-0.1.6}/LICENSE +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/pyproject.toml +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/setup.cfg +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/__main__.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/_render_fast.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/align.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/app.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/arrows.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/bonds.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/camera.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/data/c60.xyz +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/editor.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/elements.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/file_browser.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/gl_adapter.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/gl_render.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/glyph_font.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/glyph_mesh.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/glyph_outlines.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/glyphs.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/input.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/kitty.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/molecule.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/parsers/__init__.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/parsers/mol.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/parsers/pdb.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/parsers/xyz.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/periodic_table.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/py.typed +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/render.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/render_glyph.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/residues.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/scene.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/structures.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/templates.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/theme.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol/widget.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol.egg-info/dependency_links.txt +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol.egg-info/entry_points.txt +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol.egg-info/requires.txt +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/src/vimol.egg-info/top_level.txt +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_add_file.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_bonds.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_editing.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_file_browser.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_gl_render.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_glyphs.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_render_fast.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_scene_structures.py +0 -0
- {vimol-0.1.3 → vimol-0.1.6}/tests/test_structures.py +0 -0
vimol-0.1.6/PKG-INFO
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vimol
|
|
3
|
+
Version: 0.1.6
|
|
4
|
+
Summary: A terminal molecular viewer and editor using the Kitty graphics protocol
|
|
5
|
+
Author: vimol contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: molecular,viewer,editor,terminal,kitty,chemistry,visualization,xyz,pdb,sdf,cli,tui
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
23
|
+
Classifier: Topic :: Multimedia :: Graphics :: Viewers
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: numpy>=1.20
|
|
29
|
+
Requires-Dist: moderngl>=5.6
|
|
30
|
+
Requires-Dist: numba>=0.59
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
33
|
+
Provides-Extra: align
|
|
34
|
+
Requires-Dist: scipy>=1.4; extra == "align"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# vimol
|
|
38
|
+
|
|
39
|
+
A molecular viewer and editor that runs **in your terminal**. Open an `.xyz`,
|
|
40
|
+
`.pdb`, `.mol` or `.sdf` file and get a shaded, rotatable, editable 3D
|
|
41
|
+
structure right where you are already working — over SSH, in tmux, beside your
|
|
42
|
+
editor.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install vimol
|
|
46
|
+
vimol molecule.xyz
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
That is the C60 bundled with vimol, which `vimol` opens with no arguments.
|
|
52
|
+
Drag to rotate, scroll to zoom, option-drag empty space to pan, hover an atom
|
|
53
|
+
to identify it, `?` for every binding.
|
|
54
|
+
|
|
55
|
+
You need Python 3.8 or newer and a terminal that speaks the
|
|
56
|
+
[Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) —
|
|
57
|
+
**kitty, Ghostty or WezTerm**. Rendering is pure software, with an OpenGL path
|
|
58
|
+
used automatically where a GPU is available. MIT licensed.
|
|
59
|
+
|
|
60
|
+
## Proteins out of bare coordinates
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
An `.xyz` file has no residue names, no atom names and no chains. vimol finds
|
|
65
|
+
the backbone anyway, opens the file as a ribbon, and on `6` letters every
|
|
66
|
+
residue onto it. The trp-cage above is PDB 1RIJ stripped to bare coordinates,
|
|
67
|
+
which vimol reads back as `ALQELLGQWLKDGGPSSGRPPPS` — character for character
|
|
68
|
+
the sequence in that entry.
|
|
69
|
+
|
|
70
|
+
## Overlays and RMSD
|
|
71
|
+
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
Open several files, or a trajectory, and every structure lands in one session,
|
|
75
|
+
overlaid and colour-coded down the left. `r` aligns the whole overlay onto the
|
|
76
|
+
active structure and reports each RMSD in a column beside the list — above, a
|
|
77
|
+
chignolin crystal structure and 72 CREST conformers of it. `R` runs the same
|
|
78
|
+
fit on only the atoms you pick.
|
|
79
|
+
|
|
80
|
+
## Editing
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+
|
|
84
|
+
Editing is on by default. Click a hydrogen to grow a fragment in its place, a
|
|
85
|
+
heavy atom to swap the element, empty space to start something new — one click
|
|
86
|
+
turns threonine into O-methylthreonine. Bonds, deletion, clash cleanup, undo
|
|
87
|
+
and save are a keystroke each.
|
|
88
|
+
|
|
89
|
+
## As a library
|
|
90
|
+
|
|
91
|
+

|
|
92
|
+
|
|
93
|
+
Three lines put a molecule anywhere on the screen:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import os, vimol
|
|
97
|
+
|
|
98
|
+
scene = vimol.Scene(vimol.load("c60.xyz"), 640, 380) # render it off-screen
|
|
99
|
+
scene.style.transparent = True # let the terminal through
|
|
100
|
+
os.write(1, b"\x1b[6;19H" + scene.to_kitty()) # paint at row 6, col 19
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`scene.render()` hands you the pixels as a numpy array instead and
|
|
104
|
+
`scene.to_png()` writes a file. To let the reader turn the molecule,
|
|
105
|
+
`MoleculeWidget` takes the input events you hand it and leaves the loop to you;
|
|
106
|
+
`examples/embed_demo.py` is a complete host application built that way.
|
|
107
|
+
|
|
108
|
+
## Reference
|
|
109
|
+
|
|
110
|
+
| Key | Does |
|
|
111
|
+
|---|---|
|
|
112
|
+
| Drag / arrows / `hjkl` | rotate |
|
|
113
|
+
| Wheel, `+` `-` | zoom |
|
|
114
|
+
| Right or middle drag | pan |
|
|
115
|
+
| `[` `]` | roll |
|
|
116
|
+
| `1` `2` `3` `4` | ball-and-stick, space-filling, licorice, wireframe |
|
|
117
|
+
| `5` `6` | ribbon, lettered residues (proteins) |
|
|
118
|
+
| `f` `z` | re-fit, reset the view |
|
|
119
|
+
| `m` | measure — click 2, 3 or 4 atoms for distance, angle, dihedral |
|
|
120
|
+
| `a` `x` `c` `u` `s` | append, delete, cleanup clashes, undo, save |
|
|
121
|
+
| `r` `R` | align an overlay, or align on picked atoms |
|
|
122
|
+
| `Shift+S` | ready-made selections: backbone, heavy atoms, ring system |
|
|
123
|
+
| `A` `n` `p` | add a file, next and previous structure |
|
|
124
|
+
| `Tab` | focus the structure list |
|
|
125
|
+
| `d` `g` `t` `ctrl-t` | depth cue, hi-quality, transparent background, theme |
|
|
126
|
+
| `?` `q` | help, quit |
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
vimol traj.xyz --spin --style spacefill # spin a trajectory, space-filling
|
|
130
|
+
vimol protein.pdb --style glyph # lettered residues on a ribbon
|
|
131
|
+
vimol a.xyz b.pdb --style ribbon # overlay two proteins
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Several files always open in ball-and-stick, so pass `--style ribbon` for
|
|
135
|
+
proteins. Also `--backend cpu|gl|auto`, `--theme dark|light|auto`,
|
|
136
|
+
`--rotate YAW PITCH`, `--frame N`, `--background`,
|
|
137
|
+
`--transparent`/`--opaque`, `--atom-scale`, `--bond-radius`,
|
|
138
|
+
`--no-depth-cue`, `--no-bonds`, `--bond-tolerance`, `--list-formats`.
|
|
139
|
+
`pip install vimol[align]` adds scipy, which speeds up subset alignment.
|
|
140
|
+
|
|
141
|
+
The protein, alignment and editing animations are recorded by
|
|
142
|
+
`scripts/record_demos.py`.
|
vimol-0.1.6/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# vimol
|
|
2
|
+
|
|
3
|
+
A molecular viewer and editor that runs **in your terminal**. Open an `.xyz`,
|
|
4
|
+
`.pdb`, `.mol` or `.sdf` file and get a shaded, rotatable, editable 3D
|
|
5
|
+
structure right where you are already working — over SSH, in tmux, beside your
|
|
6
|
+
editor.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install vimol
|
|
10
|
+
vimol molecule.xyz
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
That is the C60 bundled with vimol, which `vimol` opens with no arguments.
|
|
16
|
+
Drag to rotate, scroll to zoom, option-drag empty space to pan, hover an atom
|
|
17
|
+
to identify it, `?` for every binding.
|
|
18
|
+
|
|
19
|
+
You need Python 3.8 or newer and a terminal that speaks the
|
|
20
|
+
[Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) —
|
|
21
|
+
**kitty, Ghostty or WezTerm**. Rendering is pure software, with an OpenGL path
|
|
22
|
+
used automatically where a GPU is available. MIT licensed.
|
|
23
|
+
|
|
24
|
+
## Proteins out of bare coordinates
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
An `.xyz` file has no residue names, no atom names and no chains. vimol finds
|
|
29
|
+
the backbone anyway, opens the file as a ribbon, and on `6` letters every
|
|
30
|
+
residue onto it. The trp-cage above is PDB 1RIJ stripped to bare coordinates,
|
|
31
|
+
which vimol reads back as `ALQELLGQWLKDGGPSSGRPPPS` — character for character
|
|
32
|
+
the sequence in that entry.
|
|
33
|
+
|
|
34
|
+
## Overlays and RMSD
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
Open several files, or a trajectory, and every structure lands in one session,
|
|
39
|
+
overlaid and colour-coded down the left. `r` aligns the whole overlay onto the
|
|
40
|
+
active structure and reports each RMSD in a column beside the list — above, a
|
|
41
|
+
chignolin crystal structure and 72 CREST conformers of it. `R` runs the same
|
|
42
|
+
fit on only the atoms you pick.
|
|
43
|
+
|
|
44
|
+
## Editing
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
Editing is on by default. Click a hydrogen to grow a fragment in its place, a
|
|
49
|
+
heavy atom to swap the element, empty space to start something new — one click
|
|
50
|
+
turns threonine into O-methylthreonine. Bonds, deletion, clash cleanup, undo
|
|
51
|
+
and save are a keystroke each.
|
|
52
|
+
|
|
53
|
+
## As a library
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
Three lines put a molecule anywhere on the screen:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import os, vimol
|
|
61
|
+
|
|
62
|
+
scene = vimol.Scene(vimol.load("c60.xyz"), 640, 380) # render it off-screen
|
|
63
|
+
scene.style.transparent = True # let the terminal through
|
|
64
|
+
os.write(1, b"\x1b[6;19H" + scene.to_kitty()) # paint at row 6, col 19
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`scene.render()` hands you the pixels as a numpy array instead and
|
|
68
|
+
`scene.to_png()` writes a file. To let the reader turn the molecule,
|
|
69
|
+
`MoleculeWidget` takes the input events you hand it and leaves the loop to you;
|
|
70
|
+
`examples/embed_demo.py` is a complete host application built that way.
|
|
71
|
+
|
|
72
|
+
## Reference
|
|
73
|
+
|
|
74
|
+
| Key | Does |
|
|
75
|
+
|---|---|
|
|
76
|
+
| Drag / arrows / `hjkl` | rotate |
|
|
77
|
+
| Wheel, `+` `-` | zoom |
|
|
78
|
+
| Right or middle drag | pan |
|
|
79
|
+
| `[` `]` | roll |
|
|
80
|
+
| `1` `2` `3` `4` | ball-and-stick, space-filling, licorice, wireframe |
|
|
81
|
+
| `5` `6` | ribbon, lettered residues (proteins) |
|
|
82
|
+
| `f` `z` | re-fit, reset the view |
|
|
83
|
+
| `m` | measure — click 2, 3 or 4 atoms for distance, angle, dihedral |
|
|
84
|
+
| `a` `x` `c` `u` `s` | append, delete, cleanup clashes, undo, save |
|
|
85
|
+
| `r` `R` | align an overlay, or align on picked atoms |
|
|
86
|
+
| `Shift+S` | ready-made selections: backbone, heavy atoms, ring system |
|
|
87
|
+
| `A` `n` `p` | add a file, next and previous structure |
|
|
88
|
+
| `Tab` | focus the structure list |
|
|
89
|
+
| `d` `g` `t` `ctrl-t` | depth cue, hi-quality, transparent background, theme |
|
|
90
|
+
| `?` `q` | help, quit |
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
vimol traj.xyz --spin --style spacefill # spin a trajectory, space-filling
|
|
94
|
+
vimol protein.pdb --style glyph # lettered residues on a ribbon
|
|
95
|
+
vimol a.xyz b.pdb --style ribbon # overlay two proteins
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Several files always open in ball-and-stick, so pass `--style ribbon` for
|
|
99
|
+
proteins. Also `--backend cpu|gl|auto`, `--theme dark|light|auto`,
|
|
100
|
+
`--rotate YAW PITCH`, `--frame N`, `--background`,
|
|
101
|
+
`--transparent`/`--opaque`, `--atom-scale`, `--bond-radius`,
|
|
102
|
+
`--no-depth-cue`, `--no-bonds`, `--bond-tolerance`, `--list-formats`.
|
|
103
|
+
`pip install vimol[align]` adds scipy, which speeds up subset alignment.
|
|
104
|
+
|
|
105
|
+
The protein, alignment and editing animations are recorded by
|
|
106
|
+
`scripts/record_demos.py`.
|
|
@@ -36,7 +36,7 @@ from . import select
|
|
|
36
36
|
from .structures import StructureSet, Structure, Transform, AlignmentResult
|
|
37
37
|
from .align import kabsch, superpose, permutation_search, subset_search
|
|
38
38
|
|
|
39
|
-
__version__ = "0.1.
|
|
39
|
+
__version__ = "0.1.6"
|
|
40
40
|
|
|
41
41
|
__all__ = [
|
|
42
42
|
"Molecule", "VectorField", "Camera", "Renderer", "Style", "Scene", "MoleculeWidget",
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Cis/trans classification of peptide and peptoid backbone bonds.
|
|
2
|
+
|
|
3
|
+
A peptide bond's rotation is described by the omega dihedral,
|
|
4
|
+
``Cα(i)-C(i)-N(i+1)-Cα(i+1)``: near 0° it is cis, near 180° trans. This is
|
|
5
|
+
the same walk :func:`vimol.select.peptide_motifs` already builds for the
|
|
6
|
+
ribbon/RMSD backbone presets, and it does not depend on where a residue's
|
|
7
|
+
side chain attaches -- a peptoid's N-substituted backbone (no amide H) is
|
|
8
|
+
the same N-Cα-C path a peptide's is, so no separate peptoid handling is
|
|
9
|
+
needed here.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import re
|
|
15
|
+
from typing import List, Optional, Sequence, Tuple
|
|
16
|
+
|
|
17
|
+
from .editor import measure_value
|
|
18
|
+
from .molecule import Molecule
|
|
19
|
+
# The private variant: classification needs the bond graph alongside the
|
|
20
|
+
# runs to notice a cyclic backbone's ring-closing peptide bond, which the
|
|
21
|
+
# public runs-only view cannot express.
|
|
22
|
+
from .select import _peptide_motifs, ring_systems
|
|
23
|
+
|
|
24
|
+
# |omega| below this is cis, at or above it is trans -- the standard
|
|
25
|
+
# literature convention, and a strict binary split with no third bucket.
|
|
26
|
+
_CIS_MAX_ABS_OMEGA = 90.0
|
|
27
|
+
|
|
28
|
+
# A ring system at least this big, carrying amides, makes the molecule a
|
|
29
|
+
# macrocycle: a cyclic peptide/peptoid, where the ring's own amides are the
|
|
30
|
+
# conformational question and the exocyclic tail is noise. Twelve is the
|
|
31
|
+
# usual line between an ordinary ring and a macrocycle, and it sits well
|
|
32
|
+
# clear of proline's pyrrolidine (5) and of the 6-membered diketopiperazine
|
|
33
|
+
# that a cyclic dipeptide forms -- both of which stay on the linear path,
|
|
34
|
+
# where the head-to-tail closure already handles them.
|
|
35
|
+
_MACROCYCLE_MIN_ATOMS = 12
|
|
36
|
+
|
|
37
|
+
# Boltzmann population of each classification across a multi-frame file,
|
|
38
|
+
# weighted by the energy in each frame's XYZ comment line. Energies are
|
|
39
|
+
# always read as Hartree (the convention CREST/xtb/ORCA write when the
|
|
40
|
+
# comment is a bare or labeled electronic energy) -- there is deliberately
|
|
41
|
+
# no unit auto-detection: a silently wrong unit produces a distribution that
|
|
42
|
+
# still looks plausible (collapsed onto one conformer, or smeared flat),
|
|
43
|
+
# so getting it wrong is worse than not offering the feature at all.
|
|
44
|
+
TEMPERATURE_K = 298.15
|
|
45
|
+
_KB_HARTREE_PER_K = 3.166811563e-6
|
|
46
|
+
_KT_HARTREE = _KB_HARTREE_PER_K * TEMPERATURE_K
|
|
47
|
+
|
|
48
|
+
# A bare float is CREST's own convention (the whole comment IS the energy).
|
|
49
|
+
_BARE_FLOAT_RE = re.compile(r"^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$")
|
|
50
|
+
# "energy", "energy:", "energy=", "E", "E=" ... followed by the number --
|
|
51
|
+
# the xtb/ORCA-style labeled convention.
|
|
52
|
+
_LABELED_ENERGY_RE = re.compile(
|
|
53
|
+
r"(?:^|\s)(?:energy|e)\s*[:=]?\s*([+-]?\d+\.\d+(?:[eE][+-]?\d+)?)",
|
|
54
|
+
re.IGNORECASE)
|
|
55
|
+
# Fallback: a decimal-point number anywhere in the comment (excludes bare
|
|
56
|
+
# integers like a model/frame index, which a comment often also carries).
|
|
57
|
+
_DECIMAL_RE = re.compile(r"[+-]?\d+\.\d+(?:[eE][+-]?\d+)?")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_energy(comment: str) -> Optional[float]:
|
|
61
|
+
"""Pull one energy value out of an XYZ comment line, or ``None``.
|
|
62
|
+
|
|
63
|
+
Three conventions, tried in order of confidence: the whole comment is
|
|
64
|
+
the number (CREST); a label ("energy"/"E", "=" or ":" optional)
|
|
65
|
+
precedes it (xtb, ORCA); or exactly one decimal number appears
|
|
66
|
+
anywhere in the comment. Two or more equally plausible numbers with no
|
|
67
|
+
label to disambiguate them is not a confident read -- ``None``, same
|
|
68
|
+
as a comment with no number at all.
|
|
69
|
+
"""
|
|
70
|
+
text = comment.strip()
|
|
71
|
+
if not text:
|
|
72
|
+
return None
|
|
73
|
+
if _BARE_FLOAT_RE.match(text):
|
|
74
|
+
return float(text)
|
|
75
|
+
labeled = _LABELED_ENERGY_RE.search(text)
|
|
76
|
+
if labeled:
|
|
77
|
+
return float(labeled.group(1))
|
|
78
|
+
decimals = _DECIMAL_RE.findall(text)
|
|
79
|
+
if len(decimals) == 1:
|
|
80
|
+
return float(decimals[0])
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def boltzmann_breakdown(classifications: Sequence[str],
|
|
85
|
+
energies_hartree: Sequence[float],
|
|
86
|
+
min_pct: float = 1.0) -> List[Tuple[str, float]]:
|
|
87
|
+
"""Boltzmann-weighted population of each distinct classification string.
|
|
88
|
+
|
|
89
|
+
``classifications`` and ``energies_hartree`` are parallel, one pair per
|
|
90
|
+
frame -- every frame must already have a real (non-``None``)
|
|
91
|
+
classification and energy; filtering that is the caller's job, not
|
|
92
|
+
this function's. Weights are relative to the group's own minimum
|
|
93
|
+
energy (numerically stable, and the group's reference point is the
|
|
94
|
+
only one that matters for population *within* the group). Returns
|
|
95
|
+
``(classification, percentage)`` pairs, rounded to nothing here (the
|
|
96
|
+
caller rounds for display), sorted by descending population, with
|
|
97
|
+
anything under ``min_pct`` dropped -- population that round to 100%
|
|
98
|
+
across the survivors is not guaranteed, by design.
|
|
99
|
+
"""
|
|
100
|
+
if not classifications or len(classifications) != len(energies_hartree):
|
|
101
|
+
return []
|
|
102
|
+
e_min = min(energies_hartree)
|
|
103
|
+
weights = [math.exp(-(e - e_min) / _KT_HARTREE) for e in energies_hartree]
|
|
104
|
+
total = sum(weights)
|
|
105
|
+
populations: dict = {}
|
|
106
|
+
for cls, w in zip(classifications, weights):
|
|
107
|
+
populations[cls] = populations.get(cls, 0.0) + w
|
|
108
|
+
breakdown = [(cls, 100.0 * w / total) for cls, w in populations.items()]
|
|
109
|
+
breakdown = [item for item in breakdown if item[1] >= min_pct]
|
|
110
|
+
breakdown.sort(key=lambda item: item[1], reverse=True)
|
|
111
|
+
return breakdown
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _amide_ring_systems(molecule: Molecule, neighbors, symbols, carbonyls):
|
|
115
|
+
"""Macrocyclic ring systems, each with the amide C-N bonds inside it.
|
|
116
|
+
|
|
117
|
+
Returns ``[(ring_atoms, [(carbonyl_C, amide_N), ...]), ...]``, only for
|
|
118
|
+
systems of at least :data:`_MACROCYCLE_MIN_ATOMS` atoms that actually
|
|
119
|
+
carry an amide. The size test alone is not enough: a steroid or a
|
|
120
|
+
porphyrin is a big ring system with no amide in it, and restricting a
|
|
121
|
+
peptide's classification to one of those would report nothing at all.
|
|
122
|
+
|
|
123
|
+
Built from the ``neighbors`` the motif walk already returned, so a
|
|
124
|
+
frame is never bond-perceived twice -- this runs over every frame of a
|
|
125
|
+
multi-thousand-frame ensemble on one keypress.
|
|
126
|
+
"""
|
|
127
|
+
systems = []
|
|
128
|
+
for ring in ring_systems(neighbors):
|
|
129
|
+
if len(ring) < _MACROCYCLE_MIN_ATOMS:
|
|
130
|
+
continue
|
|
131
|
+
inside = set(ring)
|
|
132
|
+
amides = []
|
|
133
|
+
for carbon in ring:
|
|
134
|
+
# The motif walk's own carbonyl verdict, not a second opinion:
|
|
135
|
+
# a rule of "terminal oxygen" would also accept a hydroxyl and
|
|
136
|
+
# read hydrogen-free threonine's C-OH as an amide.
|
|
137
|
+
if carbon not in carbonyls:
|
|
138
|
+
continue
|
|
139
|
+
amides.extend((carbon, n) for n in neighbors[carbon]
|
|
140
|
+
if symbols[n] == "N" and n in inside)
|
|
141
|
+
if amides:
|
|
142
|
+
systems.append((inside, amides))
|
|
143
|
+
return systems
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _ring_omega_atoms(carbon: int, nitrogen: int, ring, neighbors):
|
|
147
|
+
"""The two Cα-side atoms of a ring amide no motif pair describes.
|
|
148
|
+
|
|
149
|
+
Each is the bond partner's *other* neighbour inside the ring. Ambiguity
|
|
150
|
+
is refused rather than guessed: in a proline fused into a macrocycle the
|
|
151
|
+
nitrogen has two ring neighbours (Cα and Cδ), and picking Cδ would flip
|
|
152
|
+
c and t on exactly the residue whose cis state people care about. Those
|
|
153
|
+
bonds are motif pairs anyway, and are classified as such before this is
|
|
154
|
+
ever reached.
|
|
155
|
+
|
|
156
|
+
KNOWN GAP: refusing drops that amide from the string entirely, which is
|
|
157
|
+
the same silent class-merge that including the lactam exists to
|
|
158
|
+
prevent. Reaching it needs a non-motif amide sitting at a ring fusion
|
|
159
|
+
point, which nothing seen so far does -- but a third state would be
|
|
160
|
+
honester than omission if something ever gets there.
|
|
161
|
+
"""
|
|
162
|
+
carbon_side = [x for x in neighbors[carbon] if x in ring and x != nitrogen]
|
|
163
|
+
nitrogen_side = [x for x in neighbors[nitrogen] if x in ring and x != carbon]
|
|
164
|
+
if len(carbon_side) != 1 or len(nitrogen_side) != 1:
|
|
165
|
+
return None
|
|
166
|
+
return carbon_side[0], nitrogen_side[0]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def classify_cis_trans(molecule: Molecule) -> Optional[str]:
|
|
170
|
+
"""One ``c``/``t`` character per peptide bond, chain by chain.
|
|
171
|
+
|
|
172
|
+
A linear chain of N residues yields N-1 characters, one per
|
|
173
|
+
Cα(i)-C(i)-N(i+1) junction; a *cyclic* chain yields N, the extra one
|
|
174
|
+
for the ring-closing bond back from the last residue's carbonyl to the
|
|
175
|
+
first residue's nitrogen -- omitting it would silently merge cyclic
|
|
176
|
+
conformers that differ only in that omega. Multiple chains (a real
|
|
177
|
+
chain break, not just a gap in the file) join with ``/``, in the order
|
|
178
|
+
the motif walk returns them. A chain of a single residue has no
|
|
179
|
+
peptide bond to classify and is dropped; ``None`` means nothing in the
|
|
180
|
+
molecule survived that filter.
|
|
181
|
+
|
|
182
|
+
A MACROCYCLE is classified on its ring alone (see
|
|
183
|
+
:data:`_MACROCYCLE_MIN_ATOMS`). In a cyclic peptide it is the ring's
|
|
184
|
+
amides that are the conformational question; a pendant tail's omegas
|
|
185
|
+
are a separate, largely independent story, and letting them into the
|
|
186
|
+
same string makes every Boltzmann class a product of the two. The ring
|
|
187
|
+
string covers every amide in the ring, including a side-chain lactam
|
|
188
|
+
that no backbone walk passes through -- that bond closes the ring, and
|
|
189
|
+
dropping it would merge conformers differing only there.
|
|
190
|
+
"""
|
|
191
|
+
runs, neighbors, symbols, carbonyls = _peptide_motifs(molecule)
|
|
192
|
+
|
|
193
|
+
def omega_char(ca_i: int, c_i: int, n_j: int, ca_j: int) -> str:
|
|
194
|
+
omega = measure_value(molecule, (ca_i, c_i, n_j, ca_j))
|
|
195
|
+
return ("c" if omega is not None and abs(omega) < _CIS_MAX_ABS_OMEGA
|
|
196
|
+
else "t")
|
|
197
|
+
|
|
198
|
+
macrocycles = _amide_ring_systems(molecule, neighbors, symbols, carbonyls)
|
|
199
|
+
if macrocycles:
|
|
200
|
+
chains = []
|
|
201
|
+
for ring, amides in macrocycles:
|
|
202
|
+
# Motif pairs first, in backbone order -- they carry real Cα
|
|
203
|
+
# atoms, and their order is the one a chemist reads the ring
|
|
204
|
+
# in. Whatever is left (a lactam) follows by atom index. The
|
|
205
|
+
# order must come from topology alone: rotating the string to
|
|
206
|
+
# some "canonical" start chosen by its own c/t values would
|
|
207
|
+
# collapse cttt and tctt into one Boltzmann class.
|
|
208
|
+
chars, done = [], set()
|
|
209
|
+
for run in runs:
|
|
210
|
+
for (_n_i, ca_i, c_i, _o_i), (n_j, ca_j, _c_j, _o_j) in zip(run, run[1:]):
|
|
211
|
+
if (c_i, n_j) in amides and (c_i, n_j) not in done:
|
|
212
|
+
done.add((c_i, n_j))
|
|
213
|
+
chars.append(omega_char(ca_i, c_i, n_j, ca_j))
|
|
214
|
+
if len(run) < 2:
|
|
215
|
+
continue
|
|
216
|
+
first_n, first_ca = run[0][0], run[0][1]
|
|
217
|
+
last_ca, last_c = run[-1][1], run[-1][2]
|
|
218
|
+
if (last_c, first_n) in amides and (last_c, first_n) not in done:
|
|
219
|
+
done.add((last_c, first_n))
|
|
220
|
+
chars.append(omega_char(last_ca, last_c, first_n, first_ca))
|
|
221
|
+
for carbon, nitrogen in sorted(set(amides) - done):
|
|
222
|
+
ends = _ring_omega_atoms(carbon, nitrogen, ring, neighbors)
|
|
223
|
+
if ends is not None:
|
|
224
|
+
chars.append(omega_char(ends[0], carbon, nitrogen, ends[1]))
|
|
225
|
+
if chars:
|
|
226
|
+
chains.append("".join(chars))
|
|
227
|
+
if chains:
|
|
228
|
+
return "/".join(chains)
|
|
229
|
+
|
|
230
|
+
chains = []
|
|
231
|
+
for run in runs:
|
|
232
|
+
if len(run) < 2:
|
|
233
|
+
continue
|
|
234
|
+
chars = [omega_char(ca_i, c_i, n_j, ca_j)
|
|
235
|
+
for (_n_i, ca_i, c_i, _o_i), (n_j, ca_j, _c_j, _o_j)
|
|
236
|
+
in zip(run, run[1:])]
|
|
237
|
+
first_n, first_ca = run[0][0], run[0][1]
|
|
238
|
+
last_ca, last_c = run[-1][1], run[-1][2]
|
|
239
|
+
if first_n in neighbors[last_c]:
|
|
240
|
+
chars.append(omega_char(last_ca, last_c, first_n, first_ca))
|
|
241
|
+
chains.append("".join(chars))
|
|
242
|
+
if not chains:
|
|
243
|
+
return None
|
|
244
|
+
return "/".join(chains)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def macrocycle_size(molecule: Molecule) -> Optional[int]:
|
|
248
|
+
"""Atom count of the macrocycle a classification is restricted to, if
|
|
249
|
+
any -- what the viewer tells the user so a string that got shorter does
|
|
250
|
+
not read as a regression.
|
|
251
|
+
|
|
252
|
+
This counts the fused ring SYSTEM, not a single cycle: a proline in the
|
|
253
|
+
ring shares its N-Cα bond with the pyrrolidine, so the two fuse and the
|
|
254
|
+
count runs three past the macrocycle's own size. Hence the viewer says
|
|
255
|
+
"N ring atoms" and not "N-membered", which would be a claim about a
|
|
256
|
+
cycle and would be wrong exactly there.
|
|
257
|
+
"""
|
|
258
|
+
runs, neighbors, symbols, carbonyls = _peptide_motifs(molecule)
|
|
259
|
+
if not runs:
|
|
260
|
+
return None
|
|
261
|
+
systems = _amide_ring_systems(molecule, neighbors, symbols, carbonyls)
|
|
262
|
+
return max((len(ring) for ring, _amides in systems), default=None)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
__all__ = ["classify_cis_trans", "macrocycle_size", "parse_energy",
|
|
266
|
+
"boltzmann_breakdown", "TEMPERATURE_K"]
|