CodeEntropy 1.0.5__tar.gz → 1.0.7__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 (60) hide show
  1. {codeentropy-1.0.5 → codeentropy-1.0.7}/.github/renovate.json +5 -3
  2. {codeentropy-1.0.5 → codeentropy-1.0.7}/.github/workflows/mdanalysis-compatibility.yaml +3 -3
  3. {codeentropy-1.0.5 → codeentropy-1.0.7}/.github/workflows/project-ci.yaml +10 -10
  4. {codeentropy-1.0.5 → codeentropy-1.0.7}/.github/workflows/release.yaml +11 -11
  5. codeentropy-1.0.7/.github/workflows/update-copyright-years-in-license-file.yaml +17 -0
  6. {codeentropy-1.0.5 → codeentropy-1.0.7}/CITATION.cff +2 -2
  7. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/__init__.py +1 -1
  8. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/config/arg_config_manager.py +1 -1
  9. codeentropy-1.0.7/CodeEntropy/dihedral_tools.py +392 -0
  10. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/entropy.py +82 -126
  11. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/group_molecules.py +0 -10
  12. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/levels.py +4 -272
  13. codeentropy-1.0.7/CodeEntropy/mda_universe_operations.py +159 -0
  14. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/run.py +21 -110
  15. {codeentropy-1.0.5 → codeentropy-1.0.7}/LICENSE +1 -1
  16. {codeentropy-1.0.5 → codeentropy-1.0.7}/PKG-INFO +24 -24
  17. {codeentropy-1.0.5 → codeentropy-1.0.7}/pyproject.toml +23 -23
  18. codeentropy-1.0.7/tests/test_CodeEntropy/test_dihedral_tools.py +571 -0
  19. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_entropy.py +323 -219
  20. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_levels.py +177 -347
  21. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_main.py +3 -2
  22. codeentropy-1.0.7/tests/test_CodeEntropy/test_mda_universe_operations.py +315 -0
  23. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_run.py +69 -280
  24. codeentropy-1.0.5/.github/workflows/renovate.yaml +0 -28
  25. {codeentropy-1.0.5 → codeentropy-1.0.7}/.github/CONTRIBUTING.md +0 -0
  26. {codeentropy-1.0.5 → codeentropy-1.0.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {codeentropy-1.0.5 → codeentropy-1.0.7}/.github/dependabot.yml +0 -0
  28. {codeentropy-1.0.5 → codeentropy-1.0.7}/.github/workflows/mdanalysis-compatibility-failure.md +0 -0
  29. {codeentropy-1.0.5 → codeentropy-1.0.7}/.gitignore +0 -0
  30. {codeentropy-1.0.5 → codeentropy-1.0.7}/.pre-commit-config.yaml +0 -0
  31. {codeentropy-1.0.5 → codeentropy-1.0.7}/CODE_OF_CONDUCT.md +0 -0
  32. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/config/__init__.py +0 -0
  33. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/config/data_logger.py +0 -0
  34. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/config/logging_config.py +0 -0
  35. {codeentropy-1.0.5 → codeentropy-1.0.7}/CodeEntropy/main.py +0 -0
  36. {codeentropy-1.0.5 → codeentropy-1.0.7}/README.md +0 -0
  37. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/Makefile +0 -0
  38. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/README.md +0 -0
  39. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/_static/README.md +0 -0
  40. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/_static/custom.css +0 -0
  41. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/_templates/README.md +0 -0
  42. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/api.rst +0 -0
  43. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/conf.py +0 -0
  44. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/config.yaml +0 -0
  45. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/developer_guide.rst +0 -0
  46. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/faq.rst +0 -0
  47. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/getting_started.rst +0 -0
  48. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/images/biosim-codeentropy_logo_grey.svg +0 -0
  49. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/index.rst +0 -0
  50. {codeentropy-1.0.5 → codeentropy-1.0.7}/docs/science.rst +0 -0
  51. {codeentropy-1.0.5 → codeentropy-1.0.7}/readthedocs.yml +0 -0
  52. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/__init__.py +0 -0
  53. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/data/__init__.py +0 -0
  54. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/data/md_A4_dna.tpr +0 -0
  55. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/data/md_A4_dna_xf.trr +0 -0
  56. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_arg_config_manager.py +0 -0
  57. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_base.py +0 -0
  58. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_data_logger.py +0 -0
  59. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_group_molecules.py +0 -0
  60. {codeentropy-1.0.5 → codeentropy-1.0.7}/tests/test_CodeEntropy/test_logging_config.py +0 -0
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "extends": [
3
3
  "config:best-practices",
4
- ":pinAllExceptPeerDependencies",
5
4
  ":dependencyDashboard",
6
5
  "group:monorepos",
7
6
  "group:recommended"
8
7
  ],
9
- "rangeStrategy": "pin",
10
- "lockFileMaintenance": {
8
+ "enabledManagers": ["pep621"],
9
+ "pep621": {
11
10
  "enabled": true
12
11
  },
12
+ "rangeStrategy": "replace",
13
+ "schedule": ["* 8 * * 1-5"],
14
+ "labels": ["dependencies"],
13
15
  "packageRules": [
14
16
  {
15
17
  "matchUpdateTypes": ["minor", "patch"],
@@ -18,10 +18,10 @@ jobs:
18
18
 
19
19
  steps:
20
20
  - name: Checkout repo
21
- uses: actions/checkout@v5
21
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
22
22
 
23
23
  - name: Set up Python ${{ matrix.python-version }}
24
- uses: actions/setup-python@v6.0.0
24
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
25
25
  with:
26
26
  python-version: ${{ matrix.python-version }}
27
27
 
@@ -36,7 +36,7 @@ jobs:
36
36
 
37
37
  - name: Create Issue on Failure
38
38
  if: failure()
39
- uses: JasonEtco/create-an-issue@v2
39
+ uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2
40
40
  env:
41
41
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42
42
  PYTHON_VERSION: ${{ matrix.python-version }}
@@ -5,7 +5,7 @@ on:
5
5
  branches: [main]
6
6
  pull_request:
7
7
  schedule:
8
- - cron: '0 8 * * 1'
8
+ - cron: '0 8 * * 1-5'
9
9
  workflow_dispatch:
10
10
 
11
11
  jobs:
@@ -18,10 +18,10 @@ jobs:
18
18
  python-version: ["3.11", "3.12", "3.13", "3.14"]
19
19
  steps:
20
20
  - name: Checkout repo
21
- uses: actions/checkout@v5
21
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
22
22
 
23
23
  - name: Set up Python ${{ matrix.python-version }}
24
- uses: actions/setup-python@v6.0.0
24
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
25
25
  with:
26
26
  python-version: ${{ matrix.python-version }}
27
27
 
@@ -32,7 +32,7 @@ jobs:
32
32
  run: pytest --cov CodeEntropy --cov-report term-missing --cov-append .
33
33
 
34
34
  - name: Coveralls GitHub Action
35
- uses: coverallsapp/github-action@v2.3.7
35
+ uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2.3.7
36
36
  with:
37
37
  github-token: ${{ secrets.GITHUB_TOKEN }}
38
38
 
@@ -40,11 +40,11 @@ jobs:
40
40
  runs-on: ubuntu-latest
41
41
  timeout-minutes: 15
42
42
  steps:
43
- - uses: actions/checkout@v5
43
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
44
44
  - name: Set up Python 3.14
45
- uses: actions/setup-python@v6.0.0
45
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
46
46
  with:
47
- python-version: 3.14
47
+ python-version: 3.14.0
48
48
  - name: Install python dependencies
49
49
  run: |
50
50
  pip install --upgrade pip
@@ -56,11 +56,11 @@ jobs:
56
56
  runs-on: ubuntu-24.04
57
57
  timeout-minutes: 15
58
58
  steps:
59
- - uses: actions/checkout@v5
59
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
60
60
  - name: Set up Python 3.14
61
- uses: actions/setup-python@v6.0.0
61
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
62
62
  with:
63
- python-version: 3.14
63
+ python-version: 3.14.0
64
64
  - name: Install python dependencies
65
65
  run: |
66
66
  pip install --upgrade pip
@@ -18,12 +18,12 @@ jobs:
18
18
  steps:
19
19
  - name: Checkout repository
20
20
  id: repo
21
- uses: actions/checkout@v5
21
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
22
22
 
23
23
  - name: Set up Python
24
- uses: actions/setup-python@v6.0.0
24
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
25
25
  with:
26
- python-version: 3.14
26
+ python-version: 3.14.0
27
27
 
28
28
  - name: Get latest release from pip
29
29
  id: latestreleased
@@ -46,7 +46,7 @@ jobs:
46
46
  steps:
47
47
 
48
48
  - name: checkout
49
- uses: actions/checkout@v5
49
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
50
50
 
51
51
  - name: Change version in repo and CITATION.cff
52
52
  run: |
@@ -61,7 +61,7 @@ jobs:
61
61
 
62
62
  - name: send PR
63
63
  id: pr_id
64
- uses: peter-evans/create-pull-request@v7.0.8
64
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
65
65
  with:
66
66
  commit-message: Update version to ${{ github.event.inputs.version }}
67
67
  branch: version-update
@@ -78,7 +78,7 @@ jobs:
78
78
  draft: false
79
79
 
80
80
  - name: auto approve review
81
- uses: hmarr/auto-approve-action@v4.0.0
81
+ uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4.0.0
82
82
  with:
83
83
  pull-request-number: ${{ steps.pr_id.outputs.pull-request-number }}
84
84
  review-message: "Auto approved version bump PR"
@@ -95,7 +95,7 @@ jobs:
95
95
  runs-on: ubuntu-24.04
96
96
  steps:
97
97
  - name: Checkout repository
98
- uses: actions/checkout@v5
98
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
99
99
  with:
100
100
  ref: main
101
101
 
@@ -113,7 +113,7 @@ jobs:
113
113
  steps:
114
114
 
115
115
  - name: create release
116
- uses: softprops/action-gh-release@v2.4.2
116
+ uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
117
117
  with:
118
118
  name: v${{ github.event.inputs.version }}
119
119
  generate_release_notes: true
@@ -126,14 +126,14 @@ jobs:
126
126
  steps:
127
127
 
128
128
  - name: checkout
129
- uses: actions/checkout@v5
129
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
130
130
  with:
131
131
  ref: main
132
132
 
133
133
  - name: Set up Python
134
- uses: actions/setup-python@v6.0.0
134
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
135
135
  with:
136
- python-version: 3.14
136
+ python-version: 3.14.0
137
137
 
138
138
  - name: Install flit
139
139
  run: |
@@ -0,0 +1,17 @@
1
+ name: Update copyright year(s) in license file
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 3 1 1 *' # 03:00 AM on January 1
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ update-license-year:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v6
13
+ with:
14
+ fetch-depth: 0
15
+ - uses: FantasticFiasco/action-update-license-year@v3
16
+ with:
17
+ token: ${{ secrets.GITHUB_TOKEN }}
@@ -76,5 +76,5 @@ keywords:
76
76
  - biomolecular simulations
77
77
  - protein flexibility
78
78
  license: MIT
79
- version: 1.0.5
80
- date-released: '2025-11-18'
79
+ version: 1.0.7
80
+ date-released: '2026-01-09'
@@ -8,4 +8,4 @@ entropy estimates, supporting a wide range of applications in molecular simulati
8
8
  and statistical mechanics.
9
9
  """
10
10
 
11
- __version__ = "1.0.5"
11
+ __version__ = "1.0.7"
@@ -27,7 +27,7 @@ arg_map = {
27
27
  "kcal_force_units": {
28
28
  "type": bool,
29
29
  "default": False,
30
- "help": "Set this to True if you have a separate force file with nonSI units.",
30
+ "help": "Set this to True if you have a separate force file with kcal units.",
31
31
  },
32
32
  "selection_string": {
33
33
  "type": str,
@@ -0,0 +1,392 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ from MDAnalysis.analysis.dihedrals import Dihedral
5
+ from rich.progress import (
6
+ BarColumn,
7
+ Progress,
8
+ SpinnerColumn,
9
+ TextColumn,
10
+ TimeElapsedColumn,
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class DihedralAnalysis:
17
+ """
18
+ Functions for finding dihedral angles and analysing them to get the
19
+ states needed for the conformational entropy functions.
20
+ """
21
+
22
+ def __init__(self, universe_operations=None):
23
+ """
24
+ Initialise with placeholders.
25
+ """
26
+ self._universe_operations = universe_operations
27
+ self.data_container = None
28
+ self.states_ua = None
29
+ self.states_res = None
30
+
31
+ def build_conformational_states(
32
+ self,
33
+ data_container,
34
+ levels,
35
+ groups,
36
+ start,
37
+ end,
38
+ step,
39
+ bin_width,
40
+ ):
41
+ """
42
+ Build the conformational states descriptors based on dihedral angles
43
+ needed for the calculation of the conformational entropy.
44
+ """
45
+ number_groups = len(groups)
46
+ states_ua = {}
47
+ states_res = [None] * number_groups
48
+
49
+ total_items = sum(
50
+ len(levels[mol_id]) for mols in groups.values() for mol_id in mols
51
+ )
52
+
53
+ with Progress(
54
+ SpinnerColumn(),
55
+ TextColumn("[bold blue]{task.fields[title]}", justify="right"),
56
+ BarColumn(),
57
+ TextColumn("[progress.percentage]{task.percentage:>3.1f}%"),
58
+ TimeElapsedColumn(),
59
+ ) as progress:
60
+
61
+ task = progress.add_task(
62
+ "[green]Building Conformational States...",
63
+ total=total_items,
64
+ title="Starting...",
65
+ )
66
+
67
+ for group_id in groups.keys():
68
+ molecules = groups[group_id]
69
+ mol = self._universe_operations.get_molecule_container(
70
+ data_container, molecules[0]
71
+ )
72
+ num_residues = len(mol.residues)
73
+ dihedrals_ua = [[] for _ in range(num_residues)]
74
+ peaks_ua = [{} for _ in range(num_residues)]
75
+ dihedrals_res = []
76
+ peaks_res = {}
77
+
78
+ # Identify dihedral AtomGroups
79
+ for level in levels[molecules[0]]:
80
+ if level == "united_atom":
81
+ for res_id in range(num_residues):
82
+ selection1 = mol.residues[res_id].atoms.indices[0]
83
+ selection2 = mol.residues[res_id].atoms.indices[-1]
84
+ res_container = self._universe_operations.new_U_select_atom(
85
+ mol,
86
+ f"index {selection1}:" f"{selection2}",
87
+ )
88
+ heavy_res = self._universe_operations.new_U_select_atom(
89
+ res_container, "prop mass > 1.1"
90
+ )
91
+
92
+ dihedrals_ua[res_id] = self._get_dihedrals(heavy_res, level)
93
+
94
+ elif level == "residue":
95
+ dihedrals_res = self._get_dihedrals(mol, level)
96
+
97
+ # Identify peaks
98
+ for level in levels[molecules[0]]:
99
+ if level == "united_atom":
100
+ for res_id in range(num_residues):
101
+ if len(dihedrals_ua[res_id]) == 0:
102
+ # No dihedrals means no histogram or peaks
103
+ peaks_ua[res_id] = []
104
+ else:
105
+ peaks_ua[res_id] = self._identify_peaks(
106
+ data_container,
107
+ molecules,
108
+ dihedrals_ua[res_id],
109
+ bin_width,
110
+ start,
111
+ end,
112
+ step,
113
+ )
114
+
115
+ elif level == "residue":
116
+ if len(dihedrals_res) == 0:
117
+ # No dihedrals means no histogram or peaks
118
+ peaks_res = []
119
+ else:
120
+ peaks_res = self._identify_peaks(
121
+ data_container,
122
+ molecules,
123
+ dihedrals_res,
124
+ bin_width,
125
+ start,
126
+ end,
127
+ step,
128
+ )
129
+
130
+ # Assign states for each group
131
+ for level in levels[molecules[0]]:
132
+ if level == "united_atom":
133
+ for res_id in range(num_residues):
134
+ key = (group_id, res_id)
135
+ if len(dihedrals_ua[res_id]) == 0:
136
+ # No conformational states
137
+ states_ua[key] = []
138
+ else:
139
+ states_ua[key] = self._assign_states(
140
+ data_container,
141
+ molecules,
142
+ dihedrals_ua[res_id],
143
+ peaks_ua[res_id],
144
+ start,
145
+ end,
146
+ step,
147
+ )
148
+
149
+ elif level == "residue":
150
+ if len(dihedrals_res) == 0:
151
+ # No conformational states
152
+ states_res[group_id] = []
153
+ else:
154
+ states_res[group_id] = self._assign_states(
155
+ data_container,
156
+ molecules,
157
+ dihedrals_res,
158
+ peaks_res,
159
+ start,
160
+ end,
161
+ step,
162
+ )
163
+
164
+ progress.advance(task)
165
+
166
+ return states_ua, states_res
167
+
168
+ def _get_dihedrals(self, data_container, level):
169
+ """
170
+ Define the set of dihedrals for use in the conformational entropy function.
171
+ If united atom level, the dihedrals are defined from the heavy atoms
172
+ (4 bonded atoms for 1 dihedral).
173
+ If residue level, use the bonds between residues to cast dihedrals.
174
+ Note: not using improper dihedrals only ones with 4 atoms/residues
175
+ in a linear arrangement.
176
+
177
+ Args:
178
+ data_container (MDAnalysis.Universe): system information
179
+ level (str): level of the hierarchy (should be residue or polymer)
180
+
181
+ Returns:
182
+ dihedrals (array): set of dihedrals
183
+ """
184
+ # Start with empty array
185
+ dihedrals = []
186
+ atom_groups = []
187
+
188
+ # if united atom level, read dihedrals from MDAnalysis universe
189
+ if level == "united_atom":
190
+ dihedrals = data_container.dihedrals
191
+ num_dihedrals = len(dihedrals)
192
+ for index in range(num_dihedrals):
193
+ atom_groups.append(dihedrals[index].atoms)
194
+
195
+ # if residue level, looking for dihedrals involving residues
196
+ if level == "residue":
197
+ num_residues = len(data_container.residues)
198
+ logger.debug(f"Number Residues: {num_residues}")
199
+ if num_residues < 4:
200
+ logger.debug("no residue level dihedrals")
201
+
202
+ else:
203
+ # find bonds between residues N-3:N-2 and N-1:N
204
+ for residue in range(4, num_residues + 1):
205
+ # Using MDAnalysis selection,
206
+ # assuming only one covalent bond between neighbouring residues
207
+ # TODO not written for branched polymers
208
+ atom_string = (
209
+ "resindex "
210
+ + str(residue - 4)
211
+ + " and bonded resindex "
212
+ + str(residue - 3)
213
+ )
214
+ atom1 = data_container.select_atoms(atom_string)
215
+
216
+ atom_string = (
217
+ "resindex "
218
+ + str(residue - 3)
219
+ + " and bonded resindex "
220
+ + str(residue - 4)
221
+ )
222
+ atom2 = data_container.select_atoms(atom_string)
223
+
224
+ atom_string = (
225
+ "resindex "
226
+ + str(residue - 2)
227
+ + " and bonded resindex "
228
+ + str(residue - 1)
229
+ )
230
+ atom3 = data_container.select_atoms(atom_string)
231
+
232
+ atom_string = (
233
+ "resindex "
234
+ + str(residue - 1)
235
+ + " and bonded resindex "
236
+ + str(residue - 2)
237
+ )
238
+ atom4 = data_container.select_atoms(atom_string)
239
+
240
+ atom_group = atom1 + atom2 + atom3 + atom4
241
+ atom_groups.append(atom_group)
242
+
243
+ logger.debug(f"Level: {level}, Dihedrals: {atom_groups}")
244
+
245
+ return atom_groups
246
+
247
+ def _identify_peaks(
248
+ self,
249
+ data_container,
250
+ molecules,
251
+ dihedrals,
252
+ bin_width,
253
+ start,
254
+ end,
255
+ step,
256
+ ):
257
+ """
258
+ Build a histogram of the dihedral data and identify the peaks.
259
+ This is to give the information needed for the adaptive method
260
+ of identifying dihedral states.
261
+ """
262
+ peak_values = [] * len(dihedrals)
263
+ for dihedral_index in range(len(dihedrals)):
264
+ phi = []
265
+ # get the values of the angle for the dihedral
266
+ # loop over all molecules in the averaging group
267
+ # dihedral angle values have a range from -180 to 180
268
+ for molecule in molecules:
269
+ mol = self._universe_operations.get_molecule_container(
270
+ data_container, molecule
271
+ )
272
+ number_frames = len(mol.trajectory)
273
+ dihedral_results = Dihedral(dihedrals).run()
274
+ for timestep in range(number_frames):
275
+ value = dihedral_results.results.angles[timestep][dihedral_index]
276
+
277
+ # We want postive values in range 0 to 360 to make
278
+ # the peak assignment.
279
+ # works using the fact that dihedrals have circular symetry
280
+ # (i.e. -15 degrees = +345 degrees)
281
+ if value < 0:
282
+ value += 360
283
+ phi.append(value)
284
+
285
+ # create a histogram using numpy
286
+ number_bins = int(360 / bin_width)
287
+ popul, bin_edges = np.histogram(a=phi, bins=number_bins, range=(0, 360))
288
+ bin_value = [
289
+ 0.5 * (bin_edges[i] + bin_edges[i + 1]) for i in range(0, len(popul))
290
+ ]
291
+
292
+ # identify "convex turning-points" and populate a list of peaks
293
+ # peak : a bin whose neighboring bins have smaller population
294
+ # NOTE might have problems if the peak is wide with a flat or
295
+ # sawtooth top in which case check you have a sensible bin width
296
+
297
+ peaks = []
298
+ for bin_index in range(number_bins):
299
+ # if there is no dihedrals in a bin then it cannot be a peak
300
+ if popul[bin_index] == 0:
301
+ pass
302
+ # being careful of the last bin
303
+ # (dihedrals have circular symmetry, the histogram does not)
304
+ elif (
305
+ bin_index == number_bins - 1
306
+ ): # the -1 is because the index starts with 0 not 1
307
+ if (
308
+ popul[bin_index] >= popul[bin_index - 1]
309
+ and popul[bin_index] >= popul[0]
310
+ ):
311
+ peaks.append(bin_value[bin_index])
312
+ else:
313
+ if (
314
+ popul[bin_index] >= popul[bin_index - 1]
315
+ and popul[bin_index] >= popul[bin_index + 1]
316
+ ):
317
+ peaks.append(bin_value[bin_index])
318
+
319
+ peak_values.append(peaks)
320
+
321
+ logger.debug(f"Dihedral: {dihedral_index}, Peak Values: {peak_values}")
322
+
323
+ return peak_values
324
+
325
+ def _assign_states(
326
+ self,
327
+ data_container,
328
+ molecules,
329
+ dihedrals,
330
+ peaks,
331
+ start,
332
+ end,
333
+ step,
334
+ ):
335
+ """
336
+ Turn the dihedral values into conformations based on the peaks
337
+ from the histogram.
338
+ Then combine these to form states for each molecule.
339
+ """
340
+ states = None
341
+
342
+ # get the values of the angle for the dihedral
343
+ # dihedral angle values have a range from -180 to 180
344
+ for molecule in molecules:
345
+ conformations = []
346
+ mol = self._universe_operations.get_molecule_container(
347
+ data_container, molecule
348
+ )
349
+ number_frames = len(mol.trajectory)
350
+ dihedral_results = Dihedral(dihedrals).run()
351
+ for dihedral_index in range(len(dihedrals)):
352
+ conformation = []
353
+ for timestep in range(number_frames):
354
+ value = dihedral_results.results.angles[timestep][dihedral_index]
355
+
356
+ # We want postive values in range 0 to 360 to make
357
+ # the peak assignment.
358
+ # works using the fact that dihedrals have circular symetry
359
+ # (i.e. -15 degrees = +345 degrees)
360
+ if value < 0:
361
+ value += 360
362
+
363
+ # Find the turning point/peak that the snapshot is closest to.
364
+ distances = [abs(value - peak) for peak in peaks[dihedral_index]]
365
+ conformation.append(np.argmin(distances))
366
+
367
+ logger.debug(
368
+ f"Dihedral: {dihedral_index} Conformations: {conformation}"
369
+ )
370
+ conformations.append(conformation)
371
+
372
+ # for all the dihedrals available concatenate the label of each
373
+ # dihedral into the state for that frame
374
+ mol_states = [
375
+ state
376
+ for state in (
377
+ "".join(
378
+ str(int(conformations[d][f])) for d in range(len(dihedrals))
379
+ )
380
+ for f in range(number_frames)
381
+ )
382
+ if state
383
+ ]
384
+
385
+ if states is None:
386
+ states = mol_states
387
+ else:
388
+ states.extend(mol_states)
389
+
390
+ logger.debug(f"States: {states}")
391
+
392
+ return states