Skip to content

Commit ef08d5e

Browse files
committed
add new unittest test_merge_forces and test_merge_forces_kcal_conversion to atomically test merge_foces
1 parent 444701d commit ef08d5e

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

tests/test_CodeEntropy/test_mda_universe_operations.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,144 @@ def test_get_molecule_container(self):
172172

173173
self.assertSetEqual(set(selected_indices), set(expected_indices))
174174
self.assertEqual(len(selected_indices), len(expected_indices))
175+
176+
@patch("CodeEntropy.mda_universe_operations.AnalysisFromFunction")
177+
@patch("CodeEntropy.mda_universe_operations.mda.Merge")
178+
@patch("CodeEntropy.mda_universe_operations.mda.Universe")
179+
def test_merge_forces(self, MockUniverse, MockMerge, MockAnalysisFromFunction):
180+
"""
181+
Unit test for UniverseOperations.merge_forces().
182+
183+
This test ensures that:
184+
- Two MDAnalysis Universes are created: one for coordinates
185+
(tprfile + trrfile) and one for forces (tprfile + forcefile).
186+
- Both Universes correctly return AtomGroups via select_atoms("all").
187+
- Coordinates and forces are extracted using AnalysisFromFunction.
188+
- mda.Merge is called with the coordinate AtomGroup.
189+
- The merged Universe receives the correct coordinate and force arrays
190+
through load_new().
191+
- When kcal=False, force values are passed through unchanged
192+
(no kcal→kJ conversion).
193+
- The returned universe is the same object returned by mda.Merge().
194+
"""
195+
196+
mock_u_coords = MagicMock()
197+
mock_u_force = MagicMock()
198+
MockUniverse.side_effect = [mock_u_coords, mock_u_force]
199+
200+
# Each universe returns a mock AtomGroup from select_atoms("all")
201+
mock_ag_coords = MagicMock()
202+
mock_ag_force = MagicMock()
203+
mock_u_coords.select_atoms.return_value = mock_ag_coords
204+
mock_u_force.select_atoms.return_value = mock_ag_force
205+
206+
coords = np.random.rand(5, 10, 3)
207+
forces = np.random.rand(5, 10, 3)
208+
209+
mock_coords_analysis = MagicMock()
210+
mock_coords_analysis.run.return_value.results = {"timeseries": coords}
211+
212+
mock_forces_analysis = MagicMock()
213+
mock_forces_analysis.run.return_value.results = {"timeseries": forces}
214+
215+
# Two calls: first for coordinates, second for forces
216+
MockAnalysisFromFunction.side_effect = [
217+
mock_coords_analysis,
218+
mock_forces_analysis,
219+
]
220+
221+
mock_merged = MagicMock()
222+
MockMerge.return_value = mock_merged
223+
224+
ops = UniverseOperations()
225+
result = ops.merge_forces(
226+
tprfile="topol.tpr",
227+
trrfile="traj.trr",
228+
forcefile="forces.trr",
229+
fileformat=None,
230+
kcal=False,
231+
)
232+
233+
self.assertEqual(MockUniverse.call_count, 2)
234+
MockUniverse.assert_any_call("topol.tpr", "traj.trr", format=None)
235+
MockUniverse.assert_any_call("topol.tpr", "forces.trr", format=None)
236+
237+
mock_u_coords.select_atoms.assert_called_once_with("all")
238+
mock_u_force.select_atoms.assert_called_once_with("all")
239+
240+
self.assertEqual(MockAnalysisFromFunction.call_count, 2)
241+
242+
MockMerge.assert_called_once_with(mock_ag_coords)
243+
244+
mock_merged.load_new.assert_called_once()
245+
args, kwargs = mock_merged.load_new.call_args
246+
247+
# Coordinates passed positionally
248+
np.testing.assert_array_equal(args[0], coords)
249+
250+
# Forces passed via kwargs
251+
np.testing.assert_array_equal(kwargs["forces"], forces)
252+
253+
# Finally the function returns the merged universe
254+
self.assertEqual(result, mock_merged)
255+
256+
@patch("CodeEntropy.mda_universe_operations.AnalysisFromFunction")
257+
@patch("CodeEntropy.mda_universe_operations.mda.Merge")
258+
@patch("CodeEntropy.mda_universe_operations.mda.Universe")
259+
def test_merge_forces_kcal_conversion(
260+
self, MockUniverse, MockMerge, MockAnalysisFromFunction
261+
):
262+
"""
263+
Unit test for UniverseOperations.merge_forces() covering the kcal→kJ
264+
conversion branch.
265+
266+
Verifies that:
267+
- Two Universe objects are constructed for coords and forces.
268+
- Each Universe returns an AtomGroup via select_atoms("all").
269+
- AnalysisFromFunction is called twice.
270+
- Forces are multiplied EXACTLY once by 4.184 when kcal=True.
271+
- Merge() is called with the coordinate AtomGroup.
272+
- load_new() receives the correct coordinates and converted forces.
273+
- The returned Universe is the Merge() result.
274+
"""
275+
mock_u_coords = MagicMock()
276+
mock_u_force = MagicMock()
277+
MockUniverse.side_effect = [mock_u_coords, mock_u_force]
278+
279+
mock_ag_coords = MagicMock()
280+
mock_ag_force = MagicMock()
281+
mock_u_coords.select_atoms.return_value = mock_ag_coords
282+
mock_u_force.select_atoms.return_value = mock_ag_force
283+
284+
coords = np.ones((2, 3, 3))
285+
286+
original_forces = np.ones((2, 3, 3))
287+
mock_forces_array = original_forces.copy()
288+
289+
# Mock AnalysisFromFunction return values
290+
mock_coords_analysis = MagicMock()
291+
mock_coords_analysis.run.return_value.results = {"timeseries": coords}
292+
293+
mock_forces_analysis = MagicMock()
294+
mock_forces_analysis.run.return_value.results = {
295+
"timeseries": mock_forces_array
296+
}
297+
298+
MockAnalysisFromFunction.side_effect = [
299+
mock_coords_analysis,
300+
mock_forces_analysis,
301+
]
302+
303+
mock_merged = MagicMock()
304+
MockMerge.return_value = mock_merged
305+
306+
ops = UniverseOperations()
307+
result = ops.merge_forces("t.tpr", "c.trr", "f.trr", kcal=True)
308+
309+
_, kwargs = mock_merged.load_new.call_args
310+
311+
expected_forces = original_forces * 4.184
312+
np.testing.assert_array_equal(kwargs["forces"], expected_forces)
313+
np.testing.assert_array_equal(mock_merged.load_new.call_args[0][0], coords)
314+
315+
self.assertEqual(result, mock_merged)

0 commit comments

Comments
 (0)