Skip to content

Commit df53f5e

Browse files
authored
Merge pull request #300 from CCPBioSim/278-flexible-dihedrals
Force Halving for Flexible Dihedrals
2 parents 77fcc19 + c0b0639 commit df53f5e

44 files changed

Lines changed: 785 additions & 162 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CodeEntropy/entropy/nodes/vibrational.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> dict[str, Any]
7575
groups = shared_data["groups"]
7676
levels = shared_data["levels"]
7777
fragments = shared_data["reduced_universe"].atoms.fragments
78+
flexible = shared_data["flexible_dihedrals"]
7879

7980
gid2i = self._get_group_id_to_index(shared_data)
8081

@@ -109,6 +110,7 @@ def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> dict[str, Any]
109110
residues=rep_mol.residues,
110111
force_ua=force_cov["ua"],
111112
torque_ua=torque_cov["ua"],
113+
flexible_ua=flexible["ua"],
112114
ua_frame_counts=ua_frame_counts,
113115
reporter=reporter,
114116
n_frames_default=shared_data.get("n_frames", 0),
@@ -123,11 +125,18 @@ def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> dict[str, Any]
123125
if level in ("residue", "polymer"):
124126
gi = gid2i[group_id]
125127

128+
if level == "residue":
129+
flexible_res = flexible["res"][group_id]
130+
else:
131+
flexible_res = 0 # No polymer level flexible dihedrals
132+
126133
if combined and highest and ft_cov is not None:
127134
ft_key = "res" if level == "residue" else "poly"
128135
ftmat = self._get_indexed_matrix(ft_cov.get(ft_key, []), gi)
129136

130-
pair = self._compute_ft_entropy(ve=ve, temp=temp, ftmat=ftmat)
137+
pair = self._compute_ft_entropy(
138+
ve=ve, temp=temp, ftmat=ftmat, flexible=flexible_res
139+
)
131140
self._store_results(results, group_id, level, pair)
132141
self._log_molecule_level_results(
133142
reporter, group_id, level, pair, use_ft_labels=True
@@ -143,6 +152,7 @@ def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> dict[str, Any]
143152
temp=temp,
144153
fmat=fmat,
145154
tmat=tmat,
155+
flexible=flexible_res,
146156
highest=highest,
147157
)
148158
self._store_results(results, group_id, level, pair)
@@ -217,6 +227,7 @@ def _compute_united_atom_entropy(
217227
residues: Any,
218228
force_ua: Mapping[CovKey, Any],
219229
torque_ua: Mapping[CovKey, Any],
230+
flexible_ua: Any,
220231
ua_frame_counts: Mapping[CovKey, int],
221232
reporter: Any | None,
222233
n_frames_default: int,
@@ -235,6 +246,7 @@ def _compute_united_atom_entropy(
235246
residues: Residue container/sequence for the representative molecule.
236247
force_ua: Mapping from (group_id, residue_id) to force covariance matrix.
237248
torque_ua: Mapping from (group_id, residue_id) to torque covariance matrix.
249+
flexible: Data about number of flexible dihedrals
238250
ua_frame_counts: Mapping from (group_id, residue_id) to frame counts.
239251
reporter: Optional reporter object supporting add_residue_data calls.
240252
n_frames_default: Fallback frame count if per-residue count missing.
@@ -250,12 +262,14 @@ def _compute_united_atom_entropy(
250262
key = (group_id, res_id)
251263
fmat = force_ua.get(key)
252264
tmat = torque_ua.get(key)
265+
flexible = flexible_ua.get(key)
253266

254267
pair = self._compute_force_torque_entropy(
255268
ve=ve,
256269
temp=temp,
257270
fmat=fmat,
258271
tmat=tmat,
272+
flexible=flexible,
259273
highest=highest,
260274
)
261275

@@ -290,6 +304,7 @@ def _compute_force_torque_entropy(
290304
temp: float,
291305
fmat: Any,
292306
tmat: Any,
307+
flexible: int,
293308
highest: bool,
294309
) -> EntropyPair:
295310
"""Compute vibrational entropy from separate force and torque covariances.
@@ -322,10 +337,10 @@ def _compute_force_torque_entropy(
322337
return EntropyPair(trans=0.0, rot=0.0)
323338

324339
s_trans = ve.vibrational_entropy_calculation(
325-
f, "force", temp, highest_level=highest
340+
f, "force", temp, highest_level=highest, flexible=flexible
326341
)
327342
s_rot = ve.vibrational_entropy_calculation(
328-
t, "torque", temp, highest_level=highest
343+
t, "torque", temp, highest_level=highest, flexible=flexible
329344
)
330345
return EntropyPair(trans=float(s_trans), rot=float(s_rot))
331346

@@ -335,6 +350,7 @@ def _compute_ft_entropy(
335350
ve: VibrationalEntropy,
336351
temp: float,
337352
ftmat: Any,
353+
flexible: int,
338354
) -> EntropyPair:
339355
"""Compute vibrational entropy from a combined force-torque covariance matrix.
340356
@@ -360,10 +376,10 @@ def _compute_ft_entropy(
360376
return EntropyPair(trans=0.0, rot=0.0)
361377

362378
s_trans = ve.vibrational_entropy_calculation(
363-
ft, "forcetorqueTRANS", temp, highest_level=True
379+
ft, "forcetorqueTRANS", temp, highest_level=True, flexible=flexible
364380
)
365381
s_rot = ve.vibrational_entropy_calculation(
366-
ft, "forcetorqueROT", temp, highest_level=True
382+
ft, "forcetorqueROT", temp, highest_level=True, flexible=flexible
367383
)
368384
return EntropyPair(trans=float(s_trans), rot=float(s_rot))
369385

CodeEntropy/entropy/vibrational.py

Lines changed: 34 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:
@@ -116,7 +123,12 @@ def _entropy_components(self, matrix: np.ndarray, temp: float) -> np.ndarray:
116123
Array of entropy components (J/mol/K) for each valid mode.
117124
"""
118125
lambdas = self._matrix_eigenvalues(matrix)
126+
logger.debug("lambdas: %s", lambdas)
119127
lambdas = self._convert_lambda_units(lambdas)
128+
logger.debug("lambdas converted units: %s", lambdas)
129+
if matrix_type == "force" and flexible > 0:
130+
lambdas = self._flexible_dihedral(lambdas, flexible)
131+
logger.debug("lambdas flexible halved: %s", lambdas)
120132

121133
freqs = self._frequencies_from_lambdas(lambdas, temp)
122134
if freqs.size == 0:
@@ -149,6 +161,26 @@ def _convert_lambda_units(self, lambdas: np.ndarray) -> np.ndarray:
149161
"""
150162
return self._run_manager.change_lambda_units(lambdas)
151163

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

CodeEntropy/levels/dihedrals.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def build_conformational_states(
4444
step: int,
4545
bin_width: float,
4646
progress: _RichProgressSink | None = None,
47-
) -> tuple[dict[UAKey, list[str]], list[list[str]]]:
47+
) -> tuple[dict[UAKey, list[str]], list[list[str]], dict[UAKey, int], list[int]]:
4848
"""Build conformational state labels from trajectory dihedrals.
4949
5050
This method constructs discrete conformational state descriptors used in
@@ -72,7 +72,7 @@ def build_conformational_states(
7272
and advance().
7373
7474
Returns:
75-
tuple: (states_ua, states_res)
75+
tuple: (states_ua, states_res, flexible_ua, flexible_res)
7676
7777
- states_ua: Dict mapping (group_id, local_residue_id) -> list of state
7878
labels (strings) across the analyzed trajectory.
@@ -87,6 +87,8 @@ def build_conformational_states(
8787
number_groups = len(groups)
8888
states_ua: dict[UAKey, list[str]] = {}
8989
states_res: list[list[str]] = [[] for _ in range(number_groups)]
90+
flexible_ua: dict[UAKey, int] = {}
91+
flexible_res: list[int] = [0] * number_groups
9092

9193
task: TaskID | None = None
9294
if progress is not None:
@@ -149,12 +151,14 @@ def build_conformational_states(
149151
level_list=levels[molecules[0]],
150152
states_ua=states_ua,
151153
states_res=states_res,
154+
flexible_ua=flexible_ua,
155+
flexible_res=flexible_res,
152156
)
153157

154158
if progress is not None and task is not None:
155159
progress.advance(task)
156160

157-
return states_ua, states_res
161+
return states_ua, states_res, flexible_ua, flexible_res
158162

159163
def _collect_dihedrals_for_group(
160164
self, mol: Any, level_list: list[str]
@@ -268,6 +272,7 @@ def _collect_peaks_for_group(
268272
if level == "united_atom":
269273
for res_id in range(len(dihedrals_ua)):
270274
if len(dihedrals_ua[res_id]) == 0:
275+
# No dihedrals means no peaks
271276
peaks_ua[res_id] = []
272277
else:
273278
peaks_ua[res_id] = self._identify_peaks(
@@ -282,6 +287,7 @@ def _collect_peaks_for_group(
282287

283288
elif level == "residue":
284289
if len(dihedrals_res) == 0:
290+
# No dihedrals means no peaks
285291
peaks_res = []
286292
else:
287293
peaks_res = self._identify_peaks(
@@ -353,7 +359,9 @@ def _identify_peaks(
353359
peaks = self._find_histogram_peaks(popul=popul, bin_value=bin_value)
354360
peak_values.append(peaks)
355361

356-
logger.debug(f"Dihedral: {dihedral_index}, Peak Values: {peak_values}")
362+
logger.debug(
363+
f"Dihedral: {dihedral_index}Peak Values: {peak_values[dihedral_index]}"
364+
)
357365

358366
return peak_values
359367

@@ -404,6 +412,8 @@ def _assign_states_for_group(
404412
level_list: list[str],
405413
states_ua: dict[UAKey, list[str]],
406414
states_res: list[list[str]],
415+
flexible_ua: dict[UAKey, list[int]],
416+
flexible_res: list[int],
407417
) -> None:
408418
"""Assign UA and residue states for a group into output containers."""
409419
for level in level_list:
@@ -412,8 +422,9 @@ def _assign_states_for_group(
412422
key = (group_id, res_id)
413423
if len(dihedrals_ua[res_id]) == 0:
414424
states_ua[key] = []
425+
flexible_ua[key] = 0
415426
else:
416-
states_ua[key] = self._assign_states(
427+
states_ua[key], flexible_ua[key] = self._assign_states(
417428
data_container=data_container,
418429
molecules=molecules,
419430
dihedrals=dihedrals_ua[res_id],
@@ -426,8 +437,9 @@ def _assign_states_for_group(
426437
elif level == "residue":
427438
if len(dihedrals_res) == 0:
428439
states_res[group_id] = []
440+
flexible_res[group_id] = 0
429441
else:
430-
states_res[group_id] = self._assign_states(
442+
states_res[group_id], flexible_res[group_id] = self._assign_states(
431443
data_container=data_container,
432444
molecules=molecules,
433445
dihedrals=dihedrals_res,
@@ -467,6 +479,7 @@ def _assign_states(
467479
List of state labels (strings).
468480
"""
469481
states: list[str] = []
482+
num_flexible = 0
470483

471484
for molecule in molecules:
472485
conformations: list[list[Any]] = []
@@ -477,16 +490,29 @@ def _assign_states(
477490

478491
for dihedral_index in range(len(dihedrals)):
479492
conformation: list[Any] = []
493+
494+
# Check for flexible dihedrals
495+
if len(peaks[dihedral_index]) > 1 and molecule == 0:
496+
num_flexible += 1
497+
498+
# Get conformations
480499
for timestep in range(number_frames):
481500
value = dihedral_results.results.angles[timestep][dihedral_index]
501+
# We want postive values in range 0 to 360 to make
502+
# the peak assignment.
503+
# works using the fact that dihedrals have circular symmetry
504+
# (i.e. -15 degrees = +345 degrees)
482505
if value < 0:
483506
value += 360
484507

508+
# Find the peak closest to the dihedral value
485509
distances = [abs(value - peak) for peak in peaks[dihedral_index]]
486510
conformation.append(np.argmin(distances))
487511

488512
conformations.append(conformation)
489513

514+
# Concatenate all the dihedrals in the molecule into the state
515+
# for the frame.
490516
mol_states = [
491517
state
492518
for state in (
@@ -501,4 +527,6 @@ def _assign_states(
501527
states.extend(mol_states)
502528

503529
logger.debug(f"States: {states}")
504-
return states
530+
logger.debug(f"Number of flexible dihedrals: {num_flexible}")
531+
532+
return states, num_flexible

CodeEntropy/levels/neighbors.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ def get_neighbors(self, universe, levels, groups, search_type):
9595
len(molecules) * number_frames
9696
)
9797
logger.debug(
98-
"group: {group_id}number neighbors {average_number_neighbors[group_id]}"
98+
f"group: {group_id}"
99+
f"number neighbors {average_number_neighbors[group_id]}"
99100
)
100101

101102
return average_number_neighbors
@@ -233,14 +234,13 @@ def _get_linear(self, rdkit_mol, number_heavy):
233234
Returns:
234235
linear (bool): True if molecule linear
235236
"""
236-
rdkit_heavy = Chem.RemoveHs(rdkit_mol)
237-
238237
linear = False
239238
if number_heavy == 1:
240239
linear = False
241240
elif number_heavy == 2:
242241
linear = True
243242
else:
243+
rdkit_heavy = Chem.RemoveHs(rdkit_mol)
244244
sp_count = 0
245245
for x in rdkit_heavy.GetAtoms():
246246
if x.GetHybridization() == Chem.HybridizationType.SP:

0 commit comments

Comments
 (0)