@@ -38,6 +38,8 @@ def __init__(
38
38
universe: MDAnalysis universe representing the simulation system.
39
39
data_logger: Logger for storing and exporting entropy data.
40
40
level_manager: Provides level-specific data such as matrices and dihedrals.
41
+ group_molecules: includes the grouping functions for averaging over
42
+ molecules.
41
43
"""
42
44
self ._run_manager = run_manager
43
45
self ._args = args
@@ -58,6 +60,7 @@ def execute(self):
58
60
- Computing vibrational and conformational entropies.
59
61
- Finalizing and logging results.
60
62
"""
63
+ # Set up initial information
61
64
start , end , step = self ._get_trajectory_bounds ()
62
65
number_frames = self ._get_number_frames (start , end , step )
63
66
@@ -118,6 +121,8 @@ def execute(self):
118
121
)
119
122
)
120
123
124
+ # Identify the conformational states from dihedral angles for the
125
+ # conformational entropy calculations
121
126
states_ua , states_res = self ._level_manager .build_conformational_states (
122
127
self ,
123
128
reduced_atom ,
@@ -131,6 +136,7 @@ def execute(self):
131
136
ce ,
132
137
)
133
138
139
+ # Complete the entropy calculations
134
140
self ._compute_entropies (
135
141
reduced_atom ,
136
142
levels ,
@@ -145,6 +151,7 @@ def execute(self):
145
151
ce ,
146
152
)
147
153
154
+ # Print the results in a nicely formated way
148
155
self ._finalize_molecule_results ()
149
156
self ._data_logger .log_tables ()
150
157
@@ -190,9 +197,15 @@ def _initialize_molecules(self):
190
197
- reduced_atom (Universe): The reduced atom selection.
191
198
- number_molecules (int): Number of molecules in the system.
192
199
- levels (list): List of entropy levels per molecule.
200
+ - groups (dict): Groups for averaging over molecules.
193
201
"""
202
+ # Based on the selection string, create a new MDAnalysis universe
194
203
reduced_atom = self ._get_reduced_universe ()
204
+
205
+ # Count the molecules and identify the length scale levels for each one
195
206
number_molecules , levels = self ._level_manager .select_levels (reduced_atom )
207
+
208
+ # Group the molecules for averaging
196
209
grouping = self ._args .grouping
197
210
groups = self ._group_molecules .grouping_molecules (reduced_atom , grouping )
198
211
@@ -226,14 +239,16 @@ def _compute_entropies(
226
239
227
240
Parameters:
228
241
reduced_atom (Universe): The reduced atom selection from the trajectory.
229
- number_molecules (int): Number of molecules in the system.
230
242
levels (list): List of entropy levels per molecule.
243
+ groups (dict): Groups for averaging over molecules.
231
244
force_matrices (dict): Precomputed force covariance matrices.
232
245
torque_matrices (dict): Precomputed torque covariance matrices.
233
246
states_ua (dict): Dictionary to store united-atom conformational states.
234
247
states_res (list): List to store residue-level conformational states.
235
248
frames_count (dict): Dictionary to store the frame counts
236
249
number_frames (int): Total number of trajectory frames to process.
250
+ ve: Vibrational Entropy object
251
+ ce: Conformational Entropy object
237
252
"""
238
253
with Progress (
239
254
SpinnerColumn (),
@@ -363,8 +378,11 @@ def _get_reduced_universe(self):
363
378
Returns:
364
379
MDAnalysis.Universe: Selected subset of the system.
365
380
"""
381
+ # If selection string is "all" the universe does not change
366
382
if self ._args .selection_string == "all" :
367
383
return self ._universe
384
+
385
+ # Otherwise create a new (smaller) universe based on the selection
368
386
reduced = self ._run_manager .new_U_select_atom (
369
387
self ._universe , self ._args .selection_string
370
388
)
@@ -383,8 +401,11 @@ def _get_molecule_container(self, universe, molecule_id):
383
401
Returns:
384
402
MDAnalysis.Universe: Universe containing only the selected molecule.
385
403
"""
404
+ # Identify the atoms in the molecule
386
405
frag = universe .atoms .fragments [molecule_id ]
387
406
selection_string = f"index { frag .indices [0 ]} :{ frag .indices [- 1 ]} "
407
+
408
+ # Build a new universe with only the one molecule
388
409
return self ._run_manager .new_U_select_atom (universe , selection_string )
389
410
390
411
def _process_united_atom_entropy (
@@ -406,7 +427,7 @@ def _process_united_atom_entropy(
406
427
united-atom level.
407
428
408
429
Args:
409
- mol_id (int): ID of the molecule .
430
+ group_id (int): ID of the group .
410
431
mol_container (Universe): Universe for the selected molecule.
411
432
ve: VibrationalEntropy object.
412
433
ce: ConformationalEntropy object.
@@ -415,43 +436,56 @@ def _process_united_atom_entropy(
415
436
n_frames (int): Number of trajectory frames.
416
437
frame_counts: Number of frames counted
417
438
highest (bool): Whether this is the highest level of resolution for
418
- the molecule.
439
+ the molecule.
440
+ number_frames (int): The number of frames analysed.
419
441
"""
420
442
S_trans , S_rot , S_conf = 0 , 0 , 0
421
443
444
+ # The united atom entropy is calculated separately for each residue
445
+ # This is to allow residue by residue information
446
+ # and prevents the matrices from becoming too large
422
447
for residue_id , residue in enumerate (mol_container .residues ):
423
448
424
449
key = (group_id , residue_id )
425
450
451
+ # Find the relevant force and torque matrices and tidy them up
452
+ # by removing rows and columns that are all zeros
426
453
f_matrix = force_matrix [key ]
427
454
f_matrix = self ._level_manager .filter_zero_rows_columns (f_matrix )
428
455
429
456
t_matrix = torque_matrix [key ]
430
457
t_matrix = self ._level_manager .filter_zero_rows_columns (t_matrix )
431
458
459
+ # Calculate the vibrational entropy
432
460
S_trans_res = ve .vibrational_entropy_calculation (
433
461
f_matrix , "force" , self ._args .temperature , highest
434
462
)
435
463
S_rot_res = ve .vibrational_entropy_calculation (
436
464
t_matrix , "torque" , self ._args .temperature , highest
437
465
)
438
466
467
+ # Get the relevant conformational states
439
468
values = states [key ]
440
-
469
+ # Check if there is information in the states array
441
470
contains_non_empty_states = (
442
471
np .any (values ) if isinstance (values , np .ndarray ) else any (values )
443
472
)
444
473
474
+ # Calculate the conformational entropy
475
+ # If there are no conformational states (i.e. no dihedrals)
476
+ # then the conformational entropy is zero
445
477
S_conf_res = (
446
478
ce .conformational_entropy_calculation (values , number_frames )
447
479
if contains_non_empty_states
448
480
else 0
449
481
)
450
482
483
+ # Add the data to the united atom level entropy
451
484
S_trans += S_trans_res
452
485
S_rot += S_rot_res
453
486
S_conf += S_conf_res
454
487
488
+ # Print out the data for each residue
455
489
self ._data_logger .add_residue_data (
456
490
group_id ,
457
491
residue .resname ,
@@ -477,6 +511,7 @@ def _process_united_atom_entropy(
477
511
S_conf_res ,
478
512
)
479
513
514
+ # Print the total united atom level data for the molecule group
480
515
self ._data_logger .add_results_data (group_id , level , "Transvibrational" , S_trans )
481
516
self ._data_logger .add_results_data (group_id , level , "Rovibrational" , S_rot )
482
517
self ._data_logger .add_results_data (group_id , level , "Conformational" , S_conf )
@@ -502,8 +537,7 @@ def _process_vibrational_entropy(
502
537
Calculates vibrational entropy.
503
538
504
539
Args:
505
- mol_id (int): Molecule ID.
506
- mol_container (Universe): Selected molecule's universe.
540
+ group_id (int): Group ID.
507
541
ve: VibrationalEntropy object.
508
542
level (str): Current granularity level.
509
543
force_matrix : Force covariance matrix
@@ -512,17 +546,21 @@ def _process_vibrational_entropy(
512
546
highest (bool): Flag indicating if this is the highest granularity
513
547
level.
514
548
"""
549
+ # Find the relevant force and torque matrices and tidy them up
550
+ # by removing rows and columns that are all zeros
515
551
force_matrix = self ._level_manager .filter_zero_rows_columns (force_matrix )
516
552
517
553
torque_matrix = self ._level_manager .filter_zero_rows_columns (torque_matrix )
518
554
555
+ # Calculate the vibrational entropy
519
556
S_trans = ve .vibrational_entropy_calculation (
520
557
force_matrix , "force" , self ._args .temperature , highest
521
558
)
522
559
S_rot = ve .vibrational_entropy_calculation (
523
560
torque_matrix , "torque" , self ._args .temperature , highest
524
561
)
525
562
563
+ # Print the vibrational entropy for the molecule group
526
564
self ._data_logger .add_results_data (group_id , level , "Transvibrational" , S_trans )
527
565
self ._data_logger .add_results_data (group_id , level , "Rovibrational" , S_rot )
528
566
@@ -547,9 +585,11 @@ def _process_conformational_entropy(
547
585
mol_container (Universe): Selected molecule's universe.
548
586
ce: ConformationalEntropy object.
549
587
level (str): Level name (should be 'residue').
550
- start, end, step (int ): Frame bounds .
551
- n_frames (int): Number of frames used.
588
+ states (array ): The conformational states .
589
+ number_frames (int): Number of frames used.
552
590
"""
591
+ # Get the relevant conformational states
592
+ # Check if there is information in the states array
553
593
group_states = states [group_id ] if group_id < len (states ) else None
554
594
555
595
if group_states is not None :
@@ -561,6 +601,9 @@ def _process_conformational_entropy(
561
601
else :
562
602
contains_state_data = False
563
603
604
+ # Calculate the conformational entropy
605
+ # If there are no conformational states (i.e. no dihedrals)
606
+ # then the conformational entropy is zero
564
607
S_conf = (
565
608
ce .conformational_entropy_calculation (group_states , number_frames )
566
609
if contains_state_data
@@ -784,14 +827,12 @@ def frequency_calculation(self, lambdas, temp):
784
827
785
828
frequency=sqrt(λ/kT)/2π
786
829
787
- Input
788
- -----
789
- lambdas : array of floats - eigenvalues of the covariance matrix
790
- temp: float - temperature
830
+ Args:
831
+ lambdas : array of floats - eigenvalues of the covariance matrix
832
+ temp: float - temperature
791
833
792
- Returns
793
- -------
794
- frequencies : array of floats - corresponding vibrational frequencies
834
+ Returns:
835
+ frequencies : array of floats - corresponding vibrational frequencies
795
836
"""
796
837
pi = np .pi
797
838
# get kT in Joules from given temperature
@@ -801,11 +842,16 @@ def frequency_calculation(self, lambdas, temp):
801
842
lambdas = np .array (lambdas ) # Ensure input is a NumPy array
802
843
logger .debug (f"Eigenvalues (lambdas): { lambdas } " )
803
844
845
+ # Filter out lambda values that are negative or imaginary numbers
846
+ # As these will produce supurious entropy results that can crash
847
+ # the calculation
804
848
lambdas = np .real_if_close (lambdas , tol = 1000 )
805
849
valid_mask = (
806
850
np .isreal (lambdas ) & (lambdas > 0 ) & (~ np .isclose (lambdas , 0 , atol = 1e-07 ))
807
851
)
808
852
853
+ # If any lambdas were removed by the filter, warn the user
854
+ # as this will suggest insufficient sampling in the simulation data
809
855
if len (lambdas ) > np .count_nonzero (valid_mask ):
810
856
logger .warning (
811
857
f"{ len (lambdas ) - np .count_nonzero (valid_mask )} "
@@ -827,16 +873,14 @@ def vibrational_entropy_calculation(self, matrix, matrix_type, temp, highest_lev
827
873
Physics, 2018, 116, 1965–1976 / eq. (2) in A. Chakravorty, J. Higham and
828
874
R. H. Henchman, J. Chem. Inf. Model., 2020, 60, 5540–5551.
829
875
830
- Input
831
- -----
832
- matrix : matrix - force/torque covariance matrix
833
- matrix_type: string
834
- temp: float - temperature
835
- highest_level: bool - is this the highest level of the heirarchy
876
+ Args:
877
+ matrix : matrix - force/torque covariance matrix
878
+ matrix_type: string
879
+ temp: float - temperature
880
+ highest_level: bool - is this the highest level of the heirarchy
836
881
837
- Returns
838
- -------
839
- S_vib_total : float - transvibrational/rovibrational entropy
882
+ Returns:
883
+ S_vib_total : float - transvibrational/rovibrational entropy
840
884
"""
841
885
# N beads at a level => 3N x 3N covariance matrix => 3N eigenvalues
842
886
# Get eigenvalues of the given matrix and change units to SI units
@@ -917,18 +961,18 @@ def assign_conformation(
917
961
Based on the identified TPs, states are assigned to each configuration of the
918
962
dihedral.
919
963
920
- Input
921
- -----
922
- dihedral_atom_group : the group of 4 atoms defining the dihedral
923
- number_frames : number of frames in the trajectory
924
- bin_width : the width of the histogram bit, default 30 degrees
925
- start : int, starting frame, will default to 0
926
- end : int, ending frame, will default to -1 (last frame in trajectory)
927
- step : int, spacing between frames, will default to 1
964
+ Args:
965
+ data_container (MDAnalysis Universe): data for the molecule/residue unit
966
+ dihedral (array): The dihedral angles in the unit
967
+ number_frames (int) : number of frames in the trajectory
968
+ bin_width (int) : the width of the histogram bit, default 30 degrees
969
+ start ( int): starting frame, will default to 0
970
+ end ( int): ending frame, will default to -1 (last frame in trajectory)
971
+ step ( int): spacing between frames, will default to 1
928
972
929
- Return
930
- ------
931
- A timeseries with integer labels describing the state at each point in time.
973
+ Returns:
974
+ conformations (array): A timeseries with integer labels describing the
975
+ state at each point in time.
932
976
933
977
"""
934
978
conformations = np .zeros (number_frames )
@@ -943,7 +987,7 @@ def assign_conformation(
943
987
timestep_index = timestep_index
944
988
value = dihedral .value ()
945
989
# we want postive values in range 0 to 360 to make the peak assignment
946
- # work using the fact that dihedrals have circular symetry
990
+ # works using the fact that dihedrals have circular symetry
947
991
# (i.e. -15 degrees = +345 degrees)
948
992
if value < 0 :
949
993
value += 360
@@ -958,7 +1002,8 @@ def assign_conformation(
958
1002
959
1003
# identify "convex turning-points" and populate a list of peaks
960
1004
# peak : a bin whose neighboring bins have smaller population
961
- # NOTE might have problems if the peak is wide with a flat or sawtooth top
1005
+ # NOTE might have problems if the peak is wide with a flat or sawtooth
1006
+ # top in which case check you have a sensible bin width
962
1007
peak_values = []
963
1008
964
1009
for bin_index in range (number_bins ):
@@ -1001,12 +1046,12 @@ def conformational_entropy_calculation(self, states, number_frames):
1001
1046
1002
1047
Uses the adaptive enumeration method (AEM).
1003
1048
1004
- Input
1005
- -----
1006
- dihedrals : array - array of dihedrals in the molecule
1007
- Returns
1008
- -------
1009
- S_conf_total : float - conformational entropy
1049
+ Args:
1050
+ states (array): Conformational states in the molecule
1051
+ number_frames (int): The number of frames analysed
1052
+
1053
+ Returns:
1054
+ S_conf_total ( float) : conformational entropy
1010
1055
"""
1011
1056
1012
1057
S_conf_total = 0
0 commit comments