Skip to content

Commit 290ee42

Browse files
committed
Add frame count to final output and ensure consistency across entropy components
- Added a new `frame_count` column to the `add_residue_data` function. - Retrieved `frame_count` from `levels.py` and integrated it into `entropy.py` - Included `frame_count` for united atom level entries - Ensured consistent inclusion of frame count in water entropy calculations - Corrected aggregation logic in `_finalize_molecule_results` to ensure accurate summation of entropy values per molecule
1 parent 6ec6aed commit 290ee42

3 files changed

Lines changed: 95 additions & 48 deletions

File tree

CodeEntropy/config/data_logger.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import re
44

5+
import numpy as np
56
from rich.console import Console
67
from rich.table import Table
78

@@ -38,10 +39,14 @@ def add_results_data(self, resname, level, entropy_type, value):
3839
resname = self.clean_residue_name(resname)
3940
self.molecule_data.append((resname, level, entropy_type, value))
4041

41-
def add_residue_data(self, resid, resname, level, entropy_type, value):
42+
def add_residue_data(self, resid, resname, level, entropy_type, frame_count, value):
4243
"""Add data for residue-level entries"""
4344
resname = self.clean_residue_name(resname)
44-
self.residue_data.append([resid, resname, level, entropy_type, value])
45+
if isinstance(frame_count, np.ndarray):
46+
frame_count = frame_count.tolist()
47+
self.residue_data.append(
48+
[resid, resname, level, entropy_type, frame_count, value]
49+
)
4550

4651
def log_tables(self):
4752
"""Display rich tables in terminal"""
@@ -50,7 +55,7 @@ def log_tables(self):
5055
table = Table(
5156
title="Molecule Entropy Results", show_lines=True, expand=True
5257
)
53-
table.add_column("Residue ID", justify="center", style="bold cyan")
58+
table.add_column("Group ID", justify="center", style="bold cyan")
5459
table.add_column("Level", justify="center", style="magenta")
5560
table.add_column("Type", justify="center", style="green")
5661
table.add_column("Result (J/mol/K)", justify="center", style="yellow")
@@ -66,6 +71,7 @@ def log_tables(self):
6671
table.add_column("Residue Name", justify="center", style="cyan")
6772
table.add_column("Level", justify="center", style="magenta")
6873
table.add_column("Type", justify="center", style="green")
74+
table.add_column("Count", justify="center", style="green")
6975
table.add_column("Result (J/mol/K)", justify="center", style="yellow")
7076

7177
for row in self.residue_data:

CodeEntropy/entropy.py

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,17 @@ def execute(self):
7777
self._handle_water_entropy(start, end, step)
7878
reduced_atom, number_molecules, levels, groups = self._initialize_molecules()
7979

80-
force_matrices, torque_matrices = self._level_manager.build_covariance_matrices(
81-
self,
82-
reduced_atom,
83-
levels,
84-
groups,
85-
start,
86-
end,
87-
step,
88-
number_frames,
80+
force_matrices, torque_matrices, frame_counts = (
81+
self._level_manager.build_covariance_matrices(
82+
self,
83+
reduced_atom,
84+
levels,
85+
groups,
86+
start,
87+
end,
88+
step,
89+
number_frames,
90+
)
8991
)
9092

9193
states_ua, states_res = self._level_manager.build_conformational_states(
@@ -109,6 +111,7 @@ def execute(self):
109111
torque_matrices,
110112
states_ua,
111113
states_res,
114+
frame_counts,
112115
number_frames,
113116
ve,
114117
ce,
@@ -171,6 +174,7 @@ def _compute_entropies(
171174
torque_matrices,
172175
states_ua,
173176
states_res,
177+
frame_counts,
174178
number_frames,
175179
ve,
176180
ce,
@@ -195,6 +199,7 @@ def _compute_entropies(
195199
torque_matrices (dict): Precomputed torque covariance matrices.
196200
states_ua (dict): Dictionary to store united-atom conformational states.
197201
states_res (list): List to store residue-level conformational states.
202+
frames_count (dict): Dictionary to store the frame counts
198203
number_frames (int): Total number of trajectory frames to process.
199204
"""
200205
with Progress(
@@ -239,6 +244,7 @@ def _compute_entropies(
239244
force_matrices["ua"],
240245
torque_matrices["ua"],
241246
states_ua,
247+
frame_counts["ua"],
242248
highest,
243249
number_frames,
244250
)
@@ -300,16 +306,6 @@ def _get_number_frames(self, start, end, step):
300306
Returns:
301307
int: Total number of frames considered.
302308
"""
303-
trajectory_length = len(self._universe.trajectory)
304-
305-
if start == 0 and end == -1 and step == 1:
306-
return trajectory_length
307-
308-
if end == -1:
309-
end = trajectory_length
310-
else:
311-
end += 1
312-
313309
return math.floor((end - start) / step)
314310

315311
def _get_reduced_universe(self):
@@ -353,6 +349,7 @@ def _process_united_atom_entropy(
353349
force_matrix,
354350
torque_matrix,
355351
states,
352+
frame_counts,
356353
highest,
357354
number_frames,
358355
):
@@ -368,6 +365,7 @@ def _process_united_atom_entropy(
368365
level (str): Granularity level (should be 'united_atom').
369366
start, end, step (int): Trajectory frame parameters.
370367
n_frames (int): Number of trajectory frames.
368+
frame_counts: Number of frames counted
371369
highest (bool): Whether this is the highest level of resolution for
372370
the molecule.
373371
"""
@@ -407,21 +405,43 @@ def _process_united_atom_entropy(
407405
S_conf += S_conf_res
408406

409407
self._data_logger.add_residue_data(
410-
residue_id, residue.resname, level, "Transvibrational", S_trans_res
408+
residue_id,
409+
residue.resname,
410+
level,
411+
"Transvibrational",
412+
frame_counts[key],
413+
S_trans_res,
411414
)
412415
self._data_logger.add_residue_data(
413-
residue_id, residue.resname, level, "Rovibrational", S_rot_res
416+
residue_id,
417+
residue.resname,
418+
level,
419+
"Rovibrational",
420+
frame_counts[key],
421+
S_rot_res,
414422
)
415423
self._data_logger.add_residue_data(
416-
residue_id, residue.resname, level, "Conformational", S_conf_res
424+
residue_id,
425+
residue.resname,
426+
level,
427+
"Conformational",
428+
frame_counts[key],
429+
S_conf_res,
417430
)
418431

419432
self._data_logger.add_results_data(group_id, level, "Transvibrational", S_trans)
420433
self._data_logger.add_results_data(group_id, level, "Rovibrational", S_rot)
421434
self._data_logger.add_results_data(group_id, level, "Conformational", S_conf)
422435

423436
def _process_vibrational_entropy(
424-
self, group_id, number_frames, ve, level, force_matrix, torque_matrix, highest
437+
self,
438+
group_id,
439+
number_frames,
440+
ve,
441+
level,
442+
force_matrix,
443+
torque_matrix,
444+
highest,
425445
):
426446
"""
427447
Calculates vibrational entropy.
@@ -433,6 +453,7 @@ def _process_vibrational_entropy(
433453
level (str): Current granularity level.
434454
force_matrix : Force covariance matrix
435455
torque_matrix : Torque covariance matrix
456+
frame_count:
436457
highest (bool): Flag indicating if this is the highest granularity
437458
level.
438459
"""
@@ -486,28 +507,44 @@ def _process_conformational_entropy(
486507

487508
def _finalize_molecule_results(self):
488509
"""
489-
Aggregates and logs total entropy per molecule using residue_data grouped by
490-
resid.
510+
Aggregates and logs total entropy and frame counts per molecule.
491511
"""
492512
entropy_by_molecule = defaultdict(float)
493-
494-
for mol_id, level, entropy_type, result in self._data_logger.molecule_data:
513+
for (
514+
mol_id,
515+
level,
516+
entropy_type,
517+
result,
518+
) in self._data_logger.molecule_data:
495519
if level != "Molecule Total":
496520
try:
497521
entropy_by_molecule[mol_id] += float(result)
498522
except ValueError:
499523
logger.warning(f"Skipping invalid entry: {mol_id}, {result}")
500524

501-
for mol_id, total_entropy in entropy_by_molecule.items():
525+
# Write totals into molecule_data
526+
for mol_id in entropy_by_molecule.keys():
527+
total_entropy = entropy_by_molecule[mol_id]
528+
502529
self._data_logger.molecule_data.append(
503-
(mol_id, "Molecule Total", "Molecule Total Entropy", total_entropy)
530+
(
531+
mol_id,
532+
"Molecule Total",
533+
"Molecule Total Entropy",
534+
total_entropy,
535+
)
504536
)
505537

506538
# Save to file
507539
self._data_logger.save_dataframes_as_json(
508540
pd.DataFrame(
509541
self._data_logger.molecule_data,
510-
columns=["Molecule ID", "Level", "Type", "Result (J/mol/K)"],
542+
columns=[
543+
"Group ID",
544+
"Level",
545+
"Type",
546+
"Result (J/mol/K)",
547+
],
511548
),
512549
pd.DataFrame(
513550
self._data_logger.residue_data,
@@ -516,6 +553,7 @@ def _finalize_molecule_results(self):
516553
"Residue Name",
517554
"Level",
518555
"Type",
556+
"Frame Count",
519557
"Result (J/mol/K)",
520558
],
521559
),
@@ -532,24 +570,24 @@ def _calculate_water_entropy(self, universe, start, end, step):
532570
end (int): End frame.
533571
step (int): Step size.
534572
"""
535-
Sorient_dict, _, vibrations, _, _ = (
573+
Sorient_dict, _, vibrations, _, water_count = (
536574
GetSolvent.get_interfacial_water_orient_entropy(
537575
universe, start, end, step, self._args.temperature, parallel=True
538576
)
539577
)
540578

541579
# Log per-residue entropy using helper functions
542-
self._calculate_water_orientational_entropy(Sorient_dict)
543-
self._calculate_water_vibrational_translational_entropy(vibrations)
544-
self._calculate_water_vibrational_rotational_entropy(vibrations)
580+
self._calculate_water_orientational_entropy(Sorient_dict, water_count)
581+
self._calculate_water_vibrational_translational_entropy(vibrations, water_count)
582+
self._calculate_water_vibrational_rotational_entropy(vibrations, water_count)
545583

546584
# Aggregate entropy components per molecule
547585
results = {}
548586

549587
for row in self._data_logger.residue_data:
550588
mol_id = row[1]
551589
entropy_type = row[3].split()[0]
552-
value = float(row[4])
590+
value = float(row[5])
553591

554592
if mol_id not in results:
555593
results[mol_id] = {
@@ -570,7 +608,7 @@ def _calculate_water_entropy(self, universe, start, end, step):
570608
)
571609
total += S_component
572610

573-
def _calculate_water_orientational_entropy(self, Sorient_dict):
611+
def _calculate_water_orientational_entropy(self, Sorient_dict, water_count):
574612
"""
575613
Logs orientational entropy values directly from Sorient_dict.
576614
"""
@@ -579,10 +617,12 @@ def _calculate_water_orientational_entropy(self, Sorient_dict):
579617
if isinstance(values, list) and len(values) == 2:
580618
Sor, count = values
581619
self._data_logger.add_residue_data(
582-
resid, resname, "Water", "Orientational", Sor
620+
resid, resname, "Water", "Orientational", water_count, Sor
583621
)
584622

585-
def _calculate_water_vibrational_translational_entropy(self, vibrations):
623+
def _calculate_water_vibrational_translational_entropy(
624+
self, vibrations, water_count
625+
):
586626
"""
587627
Logs summed translational entropy values per residue-solvent pair.
588628
"""
@@ -601,10 +641,10 @@ def _calculate_water_vibrational_translational_entropy(self, vibrations):
601641
resid = -1
602642

603643
self._data_logger.add_residue_data(
604-
resid, resname, "Water", "Transvibrational", entropy
644+
resid, resname, "Water", "Transvibrational", water_count, entropy
605645
)
606646

607-
def _calculate_water_vibrational_rotational_entropy(self, vibrations):
647+
def _calculate_water_vibrational_rotational_entropy(self, vibrations, water_count):
608648
"""
609649
Logs summed rotational entropy values per residue-solvent pair.
610650
"""
@@ -623,7 +663,7 @@ def _calculate_water_vibrational_rotational_entropy(self, vibrations):
623663
resid = -1
624664

625665
self._data_logger.add_residue_data(
626-
resid, resname, "Water", "Rovibrational", entropy
666+
resid, resname, "Water", "Rovibrational", water_count, entropy
627667
)
628668

629669

@@ -813,7 +853,7 @@ def assign_conformation(
813853
for timestep_index, _ in zip(
814854
indices, data_container.trajectory[start:end:step]
815855
):
816-
timestep_index = timestep_index - start
856+
timestep_index = timestep_index
817857
value = dihedral.value()
818858
# we want postive values in range 0 to 360 to make the peak assignment
819859
# work using the fact that dihedrals have circular symetry

CodeEntropy/levels.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,7 @@ def build_covariance_matrices(
850850
group_id,
851851
level,
852852
levels[mol_id],
853-
time_index - start,
853+
time_index,
854854
number_frames,
855855
force_avg,
856856
torque_avg,
@@ -859,7 +859,7 @@ def build_covariance_matrices(
859859

860860
progress.advance(task)
861861

862-
return force_avg, torque_avg
862+
return force_avg, torque_avg, frame_counts
863863

864864
def update_force_torque_matrices(
865865
self,
@@ -975,6 +975,8 @@ def update_force_torque_matrices(
975975
force_avg[key][group_id] += (f_mat - force_avg[key][group_id]) / n
976976
torque_avg[key][group_id] += (t_mat - torque_avg[key][group_id]) / n
977977

978+
return frame_counts
979+
978980
def filter_zero_rows_columns(self, arg_matrix):
979981
"""
980982
function for removing rows and columns that contain only zeros from a matrix
@@ -1113,7 +1115,6 @@ def build_conformational_states(
11131115
res_container, "not name H*"
11141116
)
11151117
)
1116-
11171118
states = self.compute_dihedral_conformations(
11181119
heavy_res,
11191120
level,

0 commit comments

Comments
 (0)