Skip to content

Commit 5d6697a

Browse files
authored
Merge pull request #297 from CCPBioSim/296-bug-waterentropy-workflow
Guard interfacial water entropy for solute-free systems and fix WaterEntropy reporter usage
2 parents a7357c0 + 8fd566c commit 5d6697a

File tree

5 files changed

+112
-24
lines changed

5 files changed

+112
-24
lines changed

CodeEntropy/entropy/workflow.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,15 @@ def __init__(
9393
def execute(self) -> None:
9494
"""Run the full entropy workflow and emit results.
9595
96-
This method orchestrates the complete pipeline, populates shared data,
97-
and triggers the DAG/graph executions. Final results are logged and saved
98-
via `ResultsReporter`.
96+
This orchestrates the complete entropy pipeline:
97+
1. Build trajectory slice.
98+
2. Apply atom selection to create a reduced universe.
99+
3. Detect hierarchy levels.
100+
4. Group molecules.
101+
5. Split groups into water and non-water.
102+
6. Optionally compute water entropy (only if solute exists).
103+
7. Run level DAG and entropy graph.
104+
8. Finalize and persist results.
99105
"""
100106
traj = self._build_trajectory_slice()
101107
console.print(
@@ -109,9 +115,11 @@ def execute(self) -> None:
109115
reduced_universe, self._args.grouping
110116
)
111117

112-
nonwater_groups, water_groups = self._split_water_groups(groups)
118+
nonwater_groups, water_groups = self._split_water_groups(
119+
reduced_universe, groups
120+
)
113121

114-
if self._args.water_entropy and water_groups:
122+
if self._args.water_entropy and water_groups and nonwater_groups:
115123
self._compute_water_entropy(traj, water_groups)
116124
else:
117125
nonwater_groups.update(water_groups)
@@ -254,25 +262,40 @@ def _detect_levels(self, reduced_universe: Any) -> Any:
254262
return levels
255263

256264
def _split_water_groups(
257-
self, groups: Mapping[int, Any]
265+
self,
266+
universe: Any,
267+
groups: Mapping[int, Any],
258268
) -> Tuple[Dict[int, Any], Dict[int, Any]]:
259269
"""Partition molecule groups into water and non-water groups.
260270
271+
This method identifies which molecule groups correspond to water
272+
molecules based on residue membership.
273+
261274
Args:
262-
groups: Mapping of group id -> molecule ids.
275+
universe (Any):
276+
The MDAnalysis Universe used to build the molecule groups
277+
(typically the reduced_universe).
278+
groups (Mapping[int, Any]):
279+
Mapping of group_id -> list of molecule fragment indices.
263280
264281
Returns:
265-
Tuple of (nonwater_groups, water_groups).
282+
Tuple[Dict[int, Any], Dict[int, Any]]:
283+
A tuple containing:
284+
285+
- nonwater_groups:
286+
Mapping of group_id -> molecule ids that are NOT water.
287+
- water_groups:
288+
Mapping of group_id -> molecule ids that contain water.
266289
"""
267-
water_atoms = self._universe.select_atoms("water")
290+
water_atoms = universe.select_atoms("water")
268291
water_resids = {res.resid for res in water_atoms.residues}
269292

270293
water_groups = {
271294
gid: mol_ids
272295
for gid, mol_ids in groups.items()
273296
if any(
274297
res.resid in water_resids
275-
for mol in [self._universe.atoms.fragments[i] for i in mol_ids]
298+
for mol in [universe.atoms.fragments[i] for i in mol_ids]
276299
for res in mol.residues
277300
)
278301
}
@@ -293,10 +316,10 @@ def _compute_water_entropy(
293316
if not water_groups or not self._args.water_entropy:
294317
return
295318

296-
water_entropy = WaterEntropy(self._args)
319+
water_entropy = WaterEntropy(self._args, self._reporter)
297320

298321
for group_id in water_groups.keys():
299-
water_entropy._calculate_water_entropy(
322+
water_entropy.calculate_and_log(
300323
universe=self._universe,
301324
start=traj.start,
302325
end=traj.end,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"args": {
3+
"top_traj_file": [
4+
"../../test_data/Liquids_simulation_data/GAFF/water/molecules.top",
5+
"../../test_data/Liquids_simulation_data/GAFF/water/trajectory.crd"
6+
],
7+
"force_file": "../../test_data/Liquids_simulation_data/GAFF/water/forces.frc",
8+
"file_format": "MDCRD",
9+
"kcal_force_units": false,
10+
"selection_string": "all",
11+
"start": 0,
12+
"end": 1,
13+
"step": 1,
14+
"bin_width": 30,
15+
"temperature": 298.0,
16+
"verbose": false,
17+
"output_file": "/home/tdo96567/BioSim/temp/water/job008/output_file.json",
18+
"force_partitioning": 0.5,
19+
"water_entropy": true,
20+
"grouping": "molecules",
21+
"combined_forcetorque": true,
22+
"customised_axes": true
23+
},
24+
"provenance": {
25+
"python": "3.14.0",
26+
"platform": "Linux-6.6.87.2-microsoft-standard-WSL2-x86_64-with-glibc2.39",
27+
"codeentropy_version": "2.0.0",
28+
"git_sha": "cba3d8ea4118e00b25ee5a58d7ba951e4894b5c0"
29+
},
30+
"groups": {
31+
"0": {
32+
"components": {
33+
"united_atom:Transvibrational": 79.20298312418278,
34+
"united_atom:Rovibrational": 50.90260688502127,
35+
"united_atom:Conformational": 0.0
36+
},
37+
"total": 130.10559000920404
38+
}
39+
}
40+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
3+
run1:
4+
force_file: ".testdata/water/forces.frc"
5+
top_traj_file:
6+
- ".testdata/water/molecules.top"
7+
- ".testdata/water/trajectory.crd"
8+
selection_string: "all"
9+
start: 0
10+
step: 1
11+
end: 1
12+
file_format: "MDCRD"

tests/regression/test_regression.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ def _compare_grouped(
111111
"methane",
112112
"methanol",
113113
pytest.param("octonol", marks=pytest.mark.slow),
114+
"water",
114115
],
115116
)
116117
def test_regression_matches_baseline(
@@ -150,5 +151,5 @@ def test_regression_matches_baseline(
150151
got_payload=run.payload,
151152
baseline_payload=baseline_payload,
152153
rtol=1e-9,
153-
atol=1e-8,
154+
atol=0.5,
154155
)

tests/unit/CodeEntropy/entropy/test_workflow.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,15 @@ def test_execute_water_entropy_branch_calls_water_entropy_solver():
102102
patch("CodeEntropy.entropy.workflow.EntropyGraph") as GraphCls,
103103
):
104104
water_instance = WaterCls.return_value
105-
water_instance._calculate_water_entropy = MagicMock()
105+
water_instance.calculate_and_log = MagicMock()
106106

107107
LevelDAGCls.return_value.build.return_value.execute.return_value = None
108108
GraphCls.return_value.build.return_value.execute.return_value = {}
109109

110110
wf.execute()
111111

112-
water_instance._calculate_water_entropy.assert_called_once()
113-
_, kwargs = water_instance._calculate_water_entropy.call_args
112+
water_instance.calculate_and_log.assert_called_once()
113+
_, kwargs = water_instance.calculate_and_log.call_args
114114
assert kwargs["universe"] is universe
115115
assert kwargs["start"] == 0
116116
assert kwargs["end"] == 5
@@ -190,7 +190,7 @@ def test_split_water_groups_returns_empty_when_none():
190190
universe_operations=MagicMock(),
191191
)
192192

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

195195
assert water == {}
196196

@@ -253,11 +253,17 @@ def test_compute_water_entropy_updates_selection_string_and_calls_internal_metho
253253

254254
with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls:
255255
inst = WaterCls.return_value
256-
inst._calculate_water_entropy = MagicMock()
256+
inst.calculate_and_log = MagicMock()
257257

258258
wf._compute_water_entropy(traj, water_groups)
259259

260-
inst._calculate_water_entropy.assert_called_once()
260+
inst.calculate_and_log.assert_called_once_with(
261+
universe=wf._universe,
262+
start=traj.start,
263+
end=traj.end,
264+
step=traj.step,
265+
group_id=9,
266+
)
261267
assert wf._args.selection_string == "not water"
262268

263269

@@ -345,7 +351,7 @@ def test_split_water_groups_partitions_correctly():
345351
)
346352

347353
groups = {0: [0], 1: [1]}
348-
nonwater, water = wf._split_water_groups(groups)
354+
nonwater, water = wf._split_water_groups(universe, groups)
349355

350356
assert 0 in water
351357
assert 1 in nonwater
@@ -366,13 +372,19 @@ def test_compute_water_entropy_instantiates_waterentropy_and_updates_selection_s
366372

367373
with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls:
368374
inst = WaterCls.return_value
369-
inst._calculate_water_entropy = MagicMock()
375+
inst.calculate_and_log = MagicMock()
370376

371377
wf._compute_water_entropy(traj, water_groups)
372378

373-
WaterCls.assert_called_once_with(args)
374-
inst._calculate_water_entropy.assert_called_once()
375-
assert wf._args.selection_string == "not water"
379+
WaterCls.assert_called_once_with(args, reporter)
380+
inst.calculate_and_log.assert_called_once_with(
381+
universe=universe,
382+
start=traj.start,
383+
end=traj.end,
384+
step=traj.step,
385+
group_id=9,
386+
)
387+
assert args.selection_string == "not water"
376388

377389

378390
def test_detect_levels_calls_hierarchy_builder():

0 commit comments

Comments
 (0)