Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions CodeEntropy/entropy/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,15 @@ def __init__(
def execute(self) -> None:
"""Run the full entropy workflow and emit results.

This method orchestrates the complete pipeline, populates shared data,
and triggers the DAG/graph executions. Final results are logged and saved
via `ResultsReporter`.
This orchestrates the complete entropy pipeline:
1. Build trajectory slice.
2. Apply atom selection to create a reduced universe.
3. Detect hierarchy levels.
4. Group molecules.
5. Split groups into water and non-water.
6. Optionally compute water entropy (only if solute exists).
7. Run level DAG and entropy graph.
8. Finalize and persist results.
"""
traj = self._build_trajectory_slice()
console.print(
Expand All @@ -109,9 +115,11 @@ def execute(self) -> None:
reduced_universe, self._args.grouping
)

nonwater_groups, water_groups = self._split_water_groups(groups)
nonwater_groups, water_groups = self._split_water_groups(
reduced_universe, groups
)

if self._args.water_entropy and water_groups:
if self._args.water_entropy and water_groups and nonwater_groups:
self._compute_water_entropy(traj, water_groups)
else:
nonwater_groups.update(water_groups)
Expand Down Expand Up @@ -254,25 +262,40 @@ def _detect_levels(self, reduced_universe: Any) -> Any:
return levels

def _split_water_groups(
self, groups: Mapping[int, Any]
self,
universe: Any,
groups: Mapping[int, Any],
) -> Tuple[Dict[int, Any], Dict[int, Any]]:
"""Partition molecule groups into water and non-water groups.

This method identifies which molecule groups correspond to water
molecules based on residue membership.

Args:
groups: Mapping of group id -> molecule ids.
universe (Any):
The MDAnalysis Universe used to build the molecule groups
(typically the reduced_universe).
groups (Mapping[int, Any]):
Mapping of group_id -> list of molecule fragment indices.

Returns:
Tuple of (nonwater_groups, water_groups).
Tuple[Dict[int, Any], Dict[int, Any]]:
A tuple containing:

- nonwater_groups:
Mapping of group_id -> molecule ids that are NOT water.
- water_groups:
Mapping of group_id -> molecule ids that contain water.
"""
water_atoms = self._universe.select_atoms("water")
water_atoms = universe.select_atoms("water")
water_resids = {res.resid for res in water_atoms.residues}

water_groups = {
gid: mol_ids
for gid, mol_ids in groups.items()
if any(
res.resid in water_resids
for mol in [self._universe.atoms.fragments[i] for i in mol_ids]
for mol in [universe.atoms.fragments[i] for i in mol_ids]
for res in mol.residues
)
}
Expand All @@ -293,10 +316,10 @@ def _compute_water_entropy(
if not water_groups or not self._args.water_entropy:
return

water_entropy = WaterEntropy(self._args)
water_entropy = WaterEntropy(self._args, self._reporter)

for group_id in water_groups.keys():
water_entropy._calculate_water_entropy(
water_entropy.calculate_and_log(
universe=self._universe,
start=traj.start,
end=traj.end,
Expand Down
40 changes: 40 additions & 0 deletions tests/regression/baselines/water.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"args": {
"top_traj_file": [
"../../test_data/Liquids_simulation_data/GAFF/water/molecules.top",
"../../test_data/Liquids_simulation_data/GAFF/water/trajectory.crd"
],
"force_file": "../../test_data/Liquids_simulation_data/GAFF/water/forces.frc",
"file_format": "MDCRD",
"kcal_force_units": false,
"selection_string": "all",
"start": 0,
"end": 1,
"step": 1,
"bin_width": 30,
"temperature": 298.0,
"verbose": false,
"output_file": "/home/tdo96567/BioSim/temp/water/job008/output_file.json",
"force_partitioning": 0.5,
"water_entropy": true,
"grouping": "molecules",
"combined_forcetorque": true,
"customised_axes": true
},
"provenance": {
"python": "3.14.0",
"platform": "Linux-6.6.87.2-microsoft-standard-WSL2-x86_64-with-glibc2.39",
"codeentropy_version": "2.0.0",
"git_sha": "cba3d8ea4118e00b25ee5a58d7ba951e4894b5c0"
},
"groups": {
"0": {
"components": {
"united_atom:Transvibrational": 79.20298312418278,
"united_atom:Rovibrational": 50.90260688502127,
"united_atom:Conformational": 0.0
},
"total": 130.10559000920404
}
}
}
12 changes: 12 additions & 0 deletions tests/regression/configs/water/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---

run1:
force_file: ".testdata/water/forces.frc"
top_traj_file:
- ".testdata/water/molecules.top"
- ".testdata/water/trajectory.crd"
selection_string: "all"
start: 0
step: 1
end: 1
file_format: "MDCRD"
3 changes: 2 additions & 1 deletion tests/regression/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def _compare_grouped(
"methane",
"methanol",
pytest.param("octonol", marks=pytest.mark.slow),
"water",
],
)
def test_regression_matches_baseline(
Expand Down Expand Up @@ -150,5 +151,5 @@ def test_regression_matches_baseline(
got_payload=run.payload,
baseline_payload=baseline_payload,
rtol=1e-9,
atol=1e-8,
atol=0.5,
)
34 changes: 23 additions & 11 deletions tests/unit/CodeEntropy/entropy/test_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,15 @@ def test_execute_water_entropy_branch_calls_water_entropy_solver():
patch("CodeEntropy.entropy.workflow.EntropyGraph") as GraphCls,
):
water_instance = WaterCls.return_value
water_instance._calculate_water_entropy = MagicMock()
water_instance.calculate_and_log = MagicMock()

LevelDAGCls.return_value.build.return_value.execute.return_value = None
GraphCls.return_value.build.return_value.execute.return_value = {}

wf.execute()

water_instance._calculate_water_entropy.assert_called_once()
_, kwargs = water_instance._calculate_water_entropy.call_args
water_instance.calculate_and_log.assert_called_once()
_, kwargs = water_instance.calculate_and_log.call_args
assert kwargs["universe"] is universe
assert kwargs["start"] == 0
assert kwargs["end"] == 5
Expand Down Expand Up @@ -190,7 +190,7 @@ def test_split_water_groups_returns_empty_when_none():
universe_operations=MagicMock(),
)

groups, water = wf._split_water_groups({0: [1, 2]})
groups, water = wf._split_water_groups(wf._universe, {0: [1, 2]})

assert water == {}

Expand Down Expand Up @@ -253,11 +253,17 @@ def test_compute_water_entropy_updates_selection_string_and_calls_internal_metho

with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls:
inst = WaterCls.return_value
inst._calculate_water_entropy = MagicMock()
inst.calculate_and_log = MagicMock()

wf._compute_water_entropy(traj, water_groups)

inst._calculate_water_entropy.assert_called_once()
inst.calculate_and_log.assert_called_once_with(
universe=wf._universe,
start=traj.start,
end=traj.end,
step=traj.step,
group_id=9,
)
assert wf._args.selection_string == "not water"


Expand Down Expand Up @@ -345,7 +351,7 @@ def test_split_water_groups_partitions_correctly():
)

groups = {0: [0], 1: [1]}
nonwater, water = wf._split_water_groups(groups)
nonwater, water = wf._split_water_groups(universe, groups)

assert 0 in water
assert 1 in nonwater
Expand All @@ -366,13 +372,19 @@ def test_compute_water_entropy_instantiates_waterentropy_and_updates_selection_s

with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls:
inst = WaterCls.return_value
inst._calculate_water_entropy = MagicMock()
inst.calculate_and_log = MagicMock()

wf._compute_water_entropy(traj, water_groups)

WaterCls.assert_called_once_with(args)
inst._calculate_water_entropy.assert_called_once()
assert wf._args.selection_string == "not water"
WaterCls.assert_called_once_with(args, reporter)
inst.calculate_and_log.assert_called_once_with(
universe=universe,
start=traj.start,
end=traj.end,
step=traj.step,
group_id=9,
)
assert args.selection_string == "not water"


def test_detect_levels_calls_hierarchy_builder():
Expand Down