Skip to content

Commit 92a65cf

Browse files
committed
adding flexible dihedrals to vibrational entropy calculation
1 parent cba3d8e commit 92a65cf

4 files changed

Lines changed: 104 additions & 21 deletions

File tree

CodeEntropy/entropy/nodes/vibrational.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> Dict[str, Any]
7474
groups = shared_data["groups"]
7575
levels = shared_data["levels"]
7676
fragments = shared_data["reduced_universe"].atoms.fragments
77+
flexible = shared_data["flexible_dihedrals"]
7778

7879
gid2i = self._get_group_id_to_index(shared_data)
7980

@@ -108,6 +109,7 @@ def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> Dict[str, Any]
108109
residues=rep_mol.residues,
109110
force_ua=force_cov["ua"],
110111
torque_ua=torque_cov["ua"],
112+
flexible=flexible["ua"],
111113
ua_frame_counts=ua_frame_counts,
112114
reporter=reporter,
113115
n_frames_default=shared_data.get("n_frames", 0),
@@ -137,11 +139,17 @@ def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> Dict[str, Any]
137139
fmat = self._get_indexed_matrix(force_cov.get(cov_key, []), gi)
138140
tmat = self._get_indexed_matrix(torque_cov.get(cov_key, []), gi)
139141

142+
if level == "residue":
143+
flexible = flexible["res"][group_id]
144+
else:
145+
flexible = 0 # No polymer level flexible dihedrals
146+
140147
pair = self._compute_force_torque_entropy(
141148
ve=ve,
142149
temp=temp,
143150
fmat=fmat,
144151
tmat=tmat,
152+
flexible=flexible,
145153
highest=highest,
146154
)
147155
self._store_results(results, group_id, level, pair)
@@ -216,6 +224,7 @@ def _compute_united_atom_entropy(
216224
residues: Any,
217225
force_ua: Mapping[CovKey, Any],
218226
torque_ua: Mapping[CovKey, Any],
227+
flexible: Any,
219228
ua_frame_counts: Mapping[CovKey, int],
220229
reporter: Optional[Any],
221230
n_frames_default: int,
@@ -234,6 +243,7 @@ def _compute_united_atom_entropy(
234243
residues: Residue container/sequence for the representative molecule.
235244
force_ua: Mapping from (group_id, residue_id) to force covariance matrix.
236245
torque_ua: Mapping from (group_id, residue_id) to torque covariance matrix.
246+
flexible: Data about number of flexible dihedrals
237247
ua_frame_counts: Mapping from (group_id, residue_id) to frame counts.
238248
reporter: Optional reporter object supporting add_residue_data calls.
239249
n_frames_default: Fallback frame count if per-residue count missing.
@@ -249,12 +259,14 @@ def _compute_united_atom_entropy(
249259
key = (group_id, res_id)
250260
fmat = force_ua.get(key)
251261
tmat = torque_ua.get(key)
262+
flexible = flexible[key]
252263

253264
pair = self._compute_force_torque_entropy(
254265
ve=ve,
255266
temp=temp,
256267
fmat=fmat,
257268
tmat=tmat,
269+
flexible=flexible,
258270
highest=highest,
259271
)
260272

@@ -289,6 +301,7 @@ def _compute_force_torque_entropy(
289301
temp: float,
290302
fmat: Any,
291303
tmat: Any,
304+
flexible: int,
292305
highest: bool,
293306
) -> EntropyPair:
294307
"""Compute vibrational entropy from separate force and torque covariances.
@@ -321,10 +334,10 @@ def _compute_force_torque_entropy(
321334
return EntropyPair(trans=0.0, rot=0.0)
322335

323336
s_trans = ve.vibrational_entropy_calculation(
324-
f, "force", temp, highest_level=highest
337+
f, "force", temp, highest_level=highest, flexible=flexible
325338
)
326339
s_rot = ve.vibrational_entropy_calculation(
327-
t, "torque", temp, highest_level=highest
340+
t, "torque", temp, highest_level=highest, flexible=flexible
328341
)
329342
return EntropyPair(trans=float(s_trans), rot=float(s_rot))
330343

@@ -334,6 +347,7 @@ def _compute_ft_entropy(
334347
ve: VibrationalEntropy,
335348
temp: float,
336349
ftmat: Any,
350+
flexible: int,
337351
) -> EntropyPair:
338352
"""Compute vibrational entropy from a combined force-torque covariance matrix.
339353
@@ -359,10 +373,10 @@ def _compute_ft_entropy(
359373
return EntropyPair(trans=0.0, rot=0.0)
360374

361375
s_trans = ve.vibrational_entropy_calculation(
362-
ft, "forcetorqueTRANS", temp, highest_level=True
376+
ft, "forcetorqueTRANS", temp, highest_level=True, flexible=flexible
363377
)
364378
s_rot = ve.vibrational_entropy_calculation(
365-
ft, "forcetorqueROT", temp, highest_level=True
379+
ft, "forcetorqueROT", temp, highest_level=True, flexible=flexible
366380
)
367381
return EntropyPair(trans=float(s_trans), rot=float(s_rot))
368382

CodeEntropy/entropy/vibrational.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def vibrational_entropy_calculation(
6868
matrix_type: MatrixType,
6969
temp: float,
7070
highest_level: bool,
71+
flexible: int,
7172
) -> float:
7273
"""Compute vibrational entropy for the given covariance matrix.
7374
@@ -101,11 +102,17 @@ def vibrational_entropy_calculation(
101102
Raises:
102103
ValueError: If matrix_type is unknown.
103104
"""
104-
components = self._entropy_components(matrix, temp)
105+
components = self._entropy_components(matrix, matrix_type, flexible, temp)
105106
total = self._sum_components(components, matrix_type, highest_level)
106107
return float(total)
107108

108-
def _entropy_components(self, matrix: np.ndarray, temp: float) -> np.ndarray:
109+
def _entropy_components(
110+
self,
111+
matrix: np.ndarray,
112+
matrix_type: str,
113+
flexible: int,
114+
temp: float,
115+
) -> np.ndarray:
109116
"""Compute per-mode entropy components from a covariance matrix.
110117
111118
Args:
@@ -117,6 +124,8 @@ def _entropy_components(self, matrix: np.ndarray, temp: float) -> np.ndarray:
117124
"""
118125
lambdas = self._matrix_eigenvalues(matrix)
119126
lambdas = self._convert_lambda_units(lambdas)
127+
if matrix_type == "force" and flexible > 0:
128+
lambdas = self._flexible_dihedral(lambdas, flexible)
120129

121130
freqs = self._frequencies_from_lambdas(lambdas, temp)
122131
if freqs.size == 0:
@@ -149,6 +158,26 @@ def _convert_lambda_units(self, lambdas: np.ndarray) -> np.ndarray:
149158
"""
150159
return self._run_manager.change_lambda_units(lambdas)
151160

161+
def _flexible_dihedral(self, lambdas: np.ndarray, flexible: int) -> np.ndarray:
162+
"""Force halving for flexible dihedrals.
163+
164+
If N flexible dihedrals, halve the forces for the N largest eigenvalues.
165+
The matrix has force^2 so use factor of 0.25 for eigenvalues.
166+
167+
Args:
168+
lambdas: Eigenvalues
169+
flexible: the number of flexible dihedrals in the molecule
170+
171+
Returns:
172+
reduced lambdas
173+
"""
174+
halved = sorted(lambdas, reverse=True)
175+
for i in range(flexible):
176+
halved[i] = 0.25 * halved[i]
177+
lambdas = halved
178+
179+
return lambdas
180+
152181
def _frequencies_from_lambdas(self, lambdas: np.ndarray, temp: float) -> np.ndarray:
153182
"""Convert eigenvalues to frequencies with robust filtering.
154183

CodeEntropy/levels/dihedrals.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ def build_conformational_states(
8181
number_groups = len(groups)
8282
states_ua: Dict[UAKey, List[str]] = {}
8383
states_res: List[List[str]] = [None] * number_groups
84+
flexible_ua: Dict[UAKey, int] = {}
85+
flexible_res: List[int] = [None] * number_groups
8486

8587
task = None
8688
if progress is not None:
@@ -143,12 +145,14 @@ def build_conformational_states(
143145
level_list=levels[molecules[0]],
144146
states_ua=states_ua,
145147
states_res=states_res,
148+
flexible_ua=flexible_ua,
149+
flexible_res=flexible_res,
146150
)
147151

148152
if task is not None:
149153
progress.advance(task)
150154

151-
return states_ua, states_res
155+
return states_ua, states_res, flexible_ua, flexible_res
152156

153157
def _collect_dihedrals_for_group(self, mol, level_list):
154158
"""Collect UA and residue dihedral AtomGroups for a group.
@@ -260,6 +264,7 @@ def _collect_peaks_for_group(
260264
if level == "united_atom":
261265
for res_id in range(len(dihedrals_ua)):
262266
if len(dihedrals_ua[res_id]) == 0:
267+
# No dihedrals means no peaks
263268
peaks_ua[res_id] = []
264269
else:
265270
peaks_ua[res_id] = self._identify_peaks(
@@ -274,6 +279,7 @@ def _collect_peaks_for_group(
274279

275280
elif level == "residue":
276281
if len(dihedrals_res) == 0:
282+
# No dihedrals means no peaks
277283
peaks_res = []
278284
else:
279285
peaks_res = self._identify_peaks(
@@ -362,13 +368,13 @@ def _find_histogram_peaks(popul, bin_value):
362368
if bin_index == number_bins - 1:
363369
if (
364370
popul[bin_index] >= popul[bin_index - 1]
365-
and popul[bin_index] >= popul[0]
371+
and popul[bin_index] > popul[0]
366372
):
367373
peaks.append(bin_value[bin_index])
368374
else:
369375
if (
370376
popul[bin_index] >= popul[bin_index - 1]
371-
and popul[bin_index] >= popul[bin_index + 1]
377+
and popul[bin_index] > popul[bin_index + 1]
372378
):
373379
peaks.append(bin_value[bin_index])
374380

@@ -389,6 +395,8 @@ def _assign_states_for_group(
389395
level_list,
390396
states_ua,
391397
states_res,
398+
flexible_ua,
399+
flexible_res,
392400
):
393401
"""Assign UA and residue states for a group into output containers."""
394402
for level in level_list:
@@ -397,8 +405,9 @@ def _assign_states_for_group(
397405
key = (group_id, res_id)
398406
if len(dihedrals_ua[res_id]) == 0:
399407
states_ua[key] = []
408+
flexible_ua[key] = 0
400409
else:
401-
states_ua[key] = self._assign_states(
410+
states_ua[key], flexible_ua[key] = self._assign_states(
402411
data_container=data_container,
403412
molecules=molecules,
404413
dihedrals=dihedrals_ua[res_id],
@@ -411,8 +420,9 @@ def _assign_states_for_group(
411420
elif level == "residue":
412421
if len(dihedrals_res) == 0:
413422
states_res[group_id] = []
423+
flexible_res[group_id] = 0
414424
else:
415-
states_res[group_id] = self._assign_states(
425+
states_res[group_id], flexible_res[group_id] = self._assign_states(
416426
data_container=data_container,
417427
molecules=molecules,
418428
dihedrals=dihedrals_res,
@@ -452,6 +462,7 @@ def _assign_states(
452462
List of state labels (strings).
453463
"""
454464
states = None
465+
num_flexible = 0
455466

456467
for molecule in molecules:
457468
conformations = []
@@ -462,16 +473,29 @@ def _assign_states(
462473

463474
for dihedral_index in range(len(dihedrals)):
464475
conformation = []
476+
477+
# Check for flexible dihedrals
478+
if len(peaks[dihedral_index]) > 1 and molecule == 0:
479+
num_flexible += 1
480+
481+
# Get conformations
465482
for timestep in range(number_frames):
466483
value = dihedral_results.results.angles[timestep][dihedral_index]
484+
# We want postive values in range 0 to 360 to make
485+
# the peak assignment.
486+
# works using the fact that dihedrals have circular symmetry
487+
# (i.e. -15 degrees = +345 degrees)
467488
if value < 0:
468489
value += 360
469490

491+
# Find the peak closest to the dihedral value
470492
distances = [abs(value - peak) for peak in peaks[dihedral_index]]
471493
conformation.append(np.argmin(distances))
472494

473495
conformations.append(conformation)
474496

497+
# Concatenate all the dihedrals in the molecule into the state
498+
# for the frame.
475499
mol_states = [
476500
state
477501
for state in (
@@ -489,4 +513,5 @@ def _assign_states(
489513
states.extend(mol_states)
490514

491515
logger.debug("States: %s", states)
492-
return states
516+
logger.debug("Number of flexible dihedrals: %s", num_flexible)
517+
return states, num_flexible

CodeEntropy/levels/nodes/conformations.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
SharedData = Dict[str, Any]
1717
ConformationalStates = Dict[str, Any]
18+
FlexibleStates = Dict[str, Any]
1819

1920

2021
@dataclass(frozen=True)
@@ -39,10 +40,13 @@ class ComputeConformationalStatesNode:
3940
4041
Produces:
4142
shared_data["conformational_states"] = {"ua": states_ua, "res": states_res}
43+
shared_data["flexible_dihedrals"] = {"ua: flexible_ua, "res": flexible_res}
4244
4345
Where:
4446
- states_ua is a dict keyed by (group_id, local_residue_id)
4547
- states_res is a list-like structure indexed by group_id (or equivalent)
48+
- flexible_ua is a dict keyed by (group_id, local_residue_id)
49+
- flexible_res is a list-like structure indexed by group_id (or equivalent)
4650
"""
4751

4852
def __init__(self, universe_operations: Any) -> None:
@@ -79,22 +83,33 @@ def run(
7983

8084
cfg = self._build_config(shared_data)
8185

82-
states_ua, states_res = self._dihedral_analysis.build_conformational_states(
83-
data_container=u,
84-
levels=levels,
85-
groups=groups,
86-
start=cfg.start,
87-
end=cfg.end,
88-
step=cfg.step,
89-
bin_width=cfg.bin_width,
90-
progress=progress,
86+
states_ua, states_res, flexible_ua, flexible_res = (
87+
self._dihedral_analysis.build_conformational_states(
88+
data_container=u,
89+
levels=levels,
90+
groups=groups,
91+
start=cfg.start,
92+
end=cfg.end,
93+
step=cfg.step,
94+
bin_width=cfg.bin_width,
95+
progress=progress,
96+
)
9197
)
9298

99+
# Get state information into shared_data
93100
conformational_states: ConformationalStates = {
94101
"ua": states_ua,
95102
"res": states_res,
96103
}
97104
shared_data["conformational_states"] = conformational_states
105+
106+
# Get flexible_dihedral data into shared_data
107+
flexible_states: FlexibleStates = {
108+
"ua": flexible_ua,
109+
"res": flexible_res,
110+
}
111+
shared_data["flexible_dihedrals"] = flexible_states
112+
98113
return {"conformational_states": conformational_states}
99114

100115
@staticmethod

0 commit comments

Comments
 (0)