vimol 0.1.3__tar.gz → 0.1.4__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.
Files changed (64) hide show
  1. vimol-0.1.4/PKG-INFO +142 -0
  2. vimol-0.1.4/README.md +106 -0
  3. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/__init__.py +1 -1
  4. vimol-0.1.4/src/vimol/cistrans.py +142 -0
  5. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/viewer.py +370 -110
  6. vimol-0.1.4/src/vimol.egg-info/PKG-INFO +142 -0
  7. {vimol-0.1.3 → vimol-0.1.4}/src/vimol.egg-info/SOURCES.txt +2 -0
  8. vimol-0.1.4/tests/test_cistrans.py +146 -0
  9. {vimol-0.1.3 → vimol-0.1.4}/tests/test_vimol.py +505 -115
  10. vimol-0.1.3/PKG-INFO +0 -199
  11. vimol-0.1.3/README.md +0 -163
  12. vimol-0.1.3/src/vimol.egg-info/PKG-INFO +0 -199
  13. {vimol-0.1.3 → vimol-0.1.4}/LICENSE +0 -0
  14. {vimol-0.1.3 → vimol-0.1.4}/pyproject.toml +0 -0
  15. {vimol-0.1.3 → vimol-0.1.4}/setup.cfg +0 -0
  16. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/__main__.py +0 -0
  17. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/_render_fast.py +0 -0
  18. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/align.py +0 -0
  19. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/app.py +0 -0
  20. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/arrows.py +0 -0
  21. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/bonds.py +0 -0
  22. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/camera.py +0 -0
  23. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/data/c60.xyz +0 -0
  24. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/editor.py +0 -0
  25. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/elements.py +0 -0
  26. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/file_browser.py +0 -0
  27. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/gl_adapter.py +0 -0
  28. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/gl_render.py +0 -0
  29. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/glyph_font.py +0 -0
  30. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/glyph_mesh.py +0 -0
  31. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/glyph_outlines.py +0 -0
  32. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/glyphs.py +0 -0
  33. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/input.py +0 -0
  34. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/kitty.py +0 -0
  35. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/molecule.py +0 -0
  36. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/parsers/__init__.py +0 -0
  37. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/parsers/mol.py +0 -0
  38. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/parsers/pdb.py +0 -0
  39. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/parsers/xyz.py +0 -0
  40. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/periodic_table.py +0 -0
  41. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/py.typed +0 -0
  42. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/render.py +0 -0
  43. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/render_glyph.py +0 -0
  44. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/residues.py +0 -0
  45. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/scene.py +0 -0
  46. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/select.py +0 -0
  47. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/structures.py +0 -0
  48. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/templates.py +0 -0
  49. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/theme.py +0 -0
  50. {vimol-0.1.3 → vimol-0.1.4}/src/vimol/widget.py +0 -0
  51. {vimol-0.1.3 → vimol-0.1.4}/src/vimol.egg-info/dependency_links.txt +0 -0
  52. {vimol-0.1.3 → vimol-0.1.4}/src/vimol.egg-info/entry_points.txt +0 -0
  53. {vimol-0.1.3 → vimol-0.1.4}/src/vimol.egg-info/requires.txt +0 -0
  54. {vimol-0.1.3 → vimol-0.1.4}/src/vimol.egg-info/top_level.txt +0 -0
  55. {vimol-0.1.3 → vimol-0.1.4}/tests/test_add_file.py +0 -0
  56. {vimol-0.1.3 → vimol-0.1.4}/tests/test_align.py +0 -0
  57. {vimol-0.1.3 → vimol-0.1.4}/tests/test_bonds.py +0 -0
  58. {vimol-0.1.3 → vimol-0.1.4}/tests/test_editing.py +0 -0
  59. {vimol-0.1.3 → vimol-0.1.4}/tests/test_file_browser.py +0 -0
  60. {vimol-0.1.3 → vimol-0.1.4}/tests/test_gl_render.py +0 -0
  61. {vimol-0.1.3 → vimol-0.1.4}/tests/test_glyphs.py +0 -0
  62. {vimol-0.1.3 → vimol-0.1.4}/tests/test_render_fast.py +0 -0
  63. {vimol-0.1.3 → vimol-0.1.4}/tests/test_scene_structures.py +0 -0
  64. {vimol-0.1.3 → vimol-0.1.4}/tests/test_structures.py +0 -0
vimol-0.1.4/PKG-INFO ADDED
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: vimol
3
+ Version: 0.1.4
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
+ ![C60 spinning in the terminal, rendered by vimol](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/spin.gif)
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
+ ![vimol opening a bare xyz protein and switching to the lettered residue view](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/protein.gif)
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
+ ![vimol overlaying 72 CREST conformers on a reference structure and reporting each RMSD](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/align.gif)
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
+ ![vimol growing a methyl group onto threonine's side-chain hydroxyl](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/methyl.gif)
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
+ ![a C60 painted into the middle of a terminal by three lines of vimol](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/library.png)
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.4/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
+ ![C60 spinning in the terminal, rendered by vimol](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/spin.gif)
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
+ ![vimol opening a bare xyz protein and switching to the lettered residue view](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/protein.gif)
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
+ ![vimol overlaying 72 CREST conformers on a reference structure and reporting each RMSD](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/align.gif)
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
+ ![vimol growing a methyl group onto threonine's side-chain hydroxyl](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/methyl.gif)
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
+ ![a C60 painted into the middle of a terminal by three lines of vimol](https://raw.githubusercontent.com/hanimustafa/vimol/main/docs/media/library.png)
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.3"
39
+ __version__ = "0.1.4"
40
40
 
41
41
  __all__ = [
42
42
  "Molecule", "VectorField", "Camera", "Renderer", "Style", "Scene", "MoleculeWidget",
@@ -0,0 +1,142 @@
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
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
+ # Boltzmann population of each classification across a multi-frame file,
29
+ # weighted by the energy in each frame's XYZ comment line. Energies are
30
+ # always read as Hartree (the convention CREST/xtb/ORCA write when the
31
+ # comment is a bare or labeled electronic energy) -- there is deliberately
32
+ # no unit auto-detection: a silently wrong unit produces a distribution that
33
+ # still looks plausible (collapsed onto one conformer, or smeared flat),
34
+ # so getting it wrong is worse than not offering the feature at all.
35
+ TEMPERATURE_K = 298.15
36
+ _KB_HARTREE_PER_K = 3.166811563e-6
37
+ _KT_HARTREE = _KB_HARTREE_PER_K * TEMPERATURE_K
38
+
39
+ # A bare float is CREST's own convention (the whole comment IS the energy).
40
+ _BARE_FLOAT_RE = re.compile(r"^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$")
41
+ # "energy", "energy:", "energy=", "E", "E=" ... followed by the number --
42
+ # the xtb/ORCA-style labeled convention.
43
+ _LABELED_ENERGY_RE = re.compile(
44
+ r"(?:^|\s)(?:energy|e)\s*[:=]?\s*([+-]?\d+\.\d+(?:[eE][+-]?\d+)?)",
45
+ re.IGNORECASE)
46
+ # Fallback: a decimal-point number anywhere in the comment (excludes bare
47
+ # integers like a model/frame index, which a comment often also carries).
48
+ _DECIMAL_RE = re.compile(r"[+-]?\d+\.\d+(?:[eE][+-]?\d+)?")
49
+
50
+
51
+ def parse_energy(comment: str) -> Optional[float]:
52
+ """Pull one energy value out of an XYZ comment line, or ``None``.
53
+
54
+ Three conventions, tried in order of confidence: the whole comment is
55
+ the number (CREST); a label ("energy"/"E", "=" or ":" optional)
56
+ precedes it (xtb, ORCA); or exactly one decimal number appears
57
+ anywhere in the comment. Two or more equally plausible numbers with no
58
+ label to disambiguate them is not a confident read -- ``None``, same
59
+ as a comment with no number at all.
60
+ """
61
+ text = comment.strip()
62
+ if not text:
63
+ return None
64
+ if _BARE_FLOAT_RE.match(text):
65
+ return float(text)
66
+ labeled = _LABELED_ENERGY_RE.search(text)
67
+ if labeled:
68
+ return float(labeled.group(1))
69
+ decimals = _DECIMAL_RE.findall(text)
70
+ if len(decimals) == 1:
71
+ return float(decimals[0])
72
+ return None
73
+
74
+
75
+ def boltzmann_breakdown(classifications: Sequence[str],
76
+ energies_hartree: Sequence[float],
77
+ min_pct: float = 1.0) -> List[Tuple[str, float]]:
78
+ """Boltzmann-weighted population of each distinct classification string.
79
+
80
+ ``classifications`` and ``energies_hartree`` are parallel, one pair per
81
+ frame -- every frame must already have a real (non-``None``)
82
+ classification and energy; filtering that is the caller's job, not
83
+ this function's. Weights are relative to the group's own minimum
84
+ energy (numerically stable, and the group's reference point is the
85
+ only one that matters for population *within* the group). Returns
86
+ ``(classification, percentage)`` pairs, rounded to nothing here (the
87
+ caller rounds for display), sorted by descending population, with
88
+ anything under ``min_pct`` dropped -- population that round to 100%
89
+ across the survivors is not guaranteed, by design.
90
+ """
91
+ if not classifications or len(classifications) != len(energies_hartree):
92
+ return []
93
+ e_min = min(energies_hartree)
94
+ weights = [math.exp(-(e - e_min) / _KT_HARTREE) for e in energies_hartree]
95
+ total = sum(weights)
96
+ populations: dict = {}
97
+ for cls, w in zip(classifications, weights):
98
+ populations[cls] = populations.get(cls, 0.0) + w
99
+ breakdown = [(cls, 100.0 * w / total) for cls, w in populations.items()]
100
+ breakdown = [item for item in breakdown if item[1] >= min_pct]
101
+ breakdown.sort(key=lambda item: item[1], reverse=True)
102
+ return breakdown
103
+
104
+
105
+ def classify_cis_trans(molecule: Molecule) -> Optional[str]:
106
+ """One ``c``/``t`` character per peptide bond, chain by chain.
107
+
108
+ A linear chain of N residues yields N-1 characters, one per
109
+ Cα(i)-C(i)-N(i+1) junction; a *cyclic* chain yields N, the extra one
110
+ for the ring-closing bond back from the last residue's carbonyl to the
111
+ first residue's nitrogen -- omitting it would silently merge cyclic
112
+ conformers that differ only in that omega. Multiple chains (a real
113
+ chain break, not just a gap in the file) join with ``/``, in the order
114
+ the motif walk returns them. A chain of a single residue has no
115
+ peptide bond to classify and is dropped; ``None`` means nothing in the
116
+ molecule survived that filter.
117
+ """
118
+ runs, neighbors, _symbols = _peptide_motifs(molecule)
119
+ chains = []
120
+
121
+ def omega_char(ca_i: int, c_i: int, n_j: int, ca_j: int) -> str:
122
+ omega = measure_value(molecule, (ca_i, c_i, n_j, ca_j))
123
+ return ("c" if omega is not None and abs(omega) < _CIS_MAX_ABS_OMEGA
124
+ else "t")
125
+
126
+ for run in runs:
127
+ if len(run) < 2:
128
+ continue
129
+ chars = [omega_char(ca_i, c_i, n_j, ca_j)
130
+ for (_n_i, ca_i, c_i, _o_i), (n_j, ca_j, _c_j, _o_j)
131
+ in zip(run, run[1:])]
132
+ first_n, first_ca = run[0][0], run[0][1]
133
+ last_ca, last_c = run[-1][1], run[-1][2]
134
+ if first_n in neighbors[last_c]:
135
+ chars.append(omega_char(last_ca, last_c, first_n, first_ca))
136
+ chains.append("".join(chars))
137
+ if not chains:
138
+ return None
139
+ return "/".join(chains)
140
+
141
+
142
+ __all__ = ["classify_cis_trans", "parse_energy", "boltzmann_breakdown", "TEMPERATURE_K"]