@@ -31,6 +31,10 @@ def get_atom_labels(self, idx):
3131 n = int (np .asarray (self ._positions [idx ]).shape [0 ])
3232 return ['H' ] * n
3333
34+ def get_interactions (self ):
35+ # Return empty list for stub (no bonds by default)
36+ return []
37+
3438
3539@pytest .fixture
3640def simple_mdanalysis_parser ():
@@ -358,6 +362,148 @@ def test_get_coordinate_save_frequency():
358362 assert lp .get_coordinate_save_frequency (None ) is None
359363
360364
365+ def test_get_bond_list ():
366+ """Test bond list extraction from MDAnalysis interactions."""
367+ mdap = gromacs_parser .GromacsMDAnalysisParser ()
368+
369+ # Test with bond interactions
370+ mock_interactions = [
371+ {'type' : 'bond' , 'atom_indices' : [0 , 1 ], 'atom_labels' : ['O' , 'H' ]},
372+ {'type' : 'bond' , 'atom_indices' : [0 , 2 ], 'atom_labels' : ['O' , 'H' ]},
373+ {'type' : 'bond' , 'atom_indices' : [3 , 4 ], 'atom_labels' : ['O' , 'H' ]},
374+ {'type' : 'angle' , 'atom_indices' : [1 , 0 , 2 ], 'atom_labels' : ['H' , 'O' , 'H' ]},
375+ ]
376+
377+ mdap .data_object = type (
378+ 'obj' , (object ,), {'get_interactions' : lambda self : mock_interactions }
379+ )()
380+
381+ bond_list = mdap .get_bond_list ()
382+
383+ assert bond_list is not None
384+ assert isinstance (bond_list , np .ndarray )
385+ assert bond_list .shape == (3 , 2 )
386+ assert np .array_equal (bond_list [0 ], [0 , 1 ])
387+ assert np .array_equal (bond_list [1 ], [0 , 2 ])
388+ assert np .array_equal (bond_list [2 ], [3 , 4 ])
389+
390+
391+ def test_get_bond_list_no_bonds ():
392+ """Test bond list extraction when no bonds are present."""
393+ mdap = gromacs_parser .GromacsMDAnalysisParser ()
394+
395+ # Only angles, no bonds
396+ mock_interactions = [
397+ {'type' : 'angle' , 'atom_indices' : [0 , 1 , 2 ], 'atom_labels' : ['H' , 'O' , 'H' ]},
398+ ]
399+
400+ mdap .data_object = type (
401+ 'obj' , (object ,), {'get_interactions' : lambda self : mock_interactions }
402+ )()
403+
404+ bond_list = mdap .get_bond_list ()
405+ assert bond_list is None
406+
407+
408+ def test_get_bond_list_empty_interactions ():
409+ """Test bond list extraction with empty interactions."""
410+ mdap = gromacs_parser .GromacsMDAnalysisParser ()
411+
412+ mdap .data_object = type ('obj' , (object ,), {'get_interactions' : lambda self : []})()
413+
414+ bond_list = mdap .get_bond_list ()
415+ assert bond_list is None
416+
417+
418+ def test_get_bond_list_invalid_bond_indices ():
419+ """Test bond list extraction filters out invalid bond entries."""
420+ mdap = gromacs_parser .GromacsMDAnalysisParser ()
421+
422+ # Mix of valid and invalid bond interactions
423+ mock_interactions = [
424+ {'type' : 'bond' , 'atom_indices' : [0 , 1 ], 'atom_labels' : ['O' , 'H' ]},
425+ {'type' : 'bond' , 'atom_indices' : None , 'atom_labels' : ['O' , 'H' ]},
426+ {'type' : 'bond' , 'atom_indices' : [2 , 3 , 4 ], 'atom_labels' : ['O' , 'H' , 'C' ]},
427+ {'type' : 'bond' , 'atom_indices' : [4 , 5 ], 'atom_labels' : ['O' , 'H' ]},
428+ ]
429+
430+ mdap .data_object = type (
431+ 'obj' , (object ,), {'get_interactions' : lambda self : mock_interactions }
432+ )()
433+
434+ bond_list = mdap .get_bond_list ()
435+
436+ assert bond_list is not None
437+ assert bond_list .shape == (2 , 2 )
438+ assert np .array_equal (bond_list [0 ], [0 , 1 ])
439+ assert np .array_equal (bond_list [1 ], [4 , 5 ])
440+
441+
442+ def test_integration_bond_list_in_parsed_system ():
443+ """Test that bond_list is populated in parsed model_system from TPR file."""
444+ base = Path (__file__ ).parent .parent / 'data' / 'gromacs' / 'water'
445+ tpr_file = os .path .join (base , 'topol.tpr' )
446+
447+ if not os .path .exists (tpr_file ):
448+ pytest .skip (f'TPR file not found: { tpr_file } ' )
449+
450+ archive = EntryArchive ()
451+ parser = gromacs_parser .GromacsParser ()
452+ parser .parse (tpr_file , archive )
453+
454+ assert archive .data is not None
455+ assert len (archive .data .model_system ) > 0
456+
457+ system = archive .data .model_system [0 ]
458+ assert system .bond_list is not None , 'Bond list should be populated from TPR'
459+ assert isinstance (system .bond_list , np .ndarray )
460+ assert system .bond_list .shape [1 ] == 2 , 'Bond list should have shape (n_bonds, 2)'
461+ assert system .bond_list .shape [0 ] > 0 , 'Should have at least one bond'
462+ # Water molecules have 2 O-H bonds each (432 bonds for 144 water molecules)
463+ assert system .bond_list .shape [0 ] == 432
464+
465+
466+ def test_get_configurations_includes_bond_list ():
467+ """Test that get_configurations includes bond_list in first frame only."""
468+ mdap = gromacs_parser .GromacsMDAnalysisParser ()
469+ mdap ._trajectory_steps_sampled = [0 , 1 ]
470+
471+ # Mock interactions with bonds
472+ mock_interactions = [
473+ {'type' : 'bond' , 'atom_indices' : [0 , 1 ], 'atom_labels' : ['O' , 'H' ]},
474+ {'type' : 'bond' , 'atom_indices' : [0 , 2 ], 'atom_labels' : ['O' , 'H' ]},
475+ ]
476+
477+ # Mock data object
478+ class MockDataObject :
479+ def get_positions (self , idx ):
480+ return np .zeros ((3 , 3 ))
481+
482+ def get_velocities (self , idx ):
483+ return np .zeros ((3 , 3 ))
484+
485+ def get_lattice_vectors (self , idx ):
486+ return np .eye (3 )
487+
488+ def get_atom_labels (self , idx ):
489+ return ['O' , 'H' , 'H' ]
490+
491+ def get_interactions (self ):
492+ return mock_interactions
493+
494+ mdap .data_object = MockDataObject ()
495+
496+ configs = mdap .get_configurations ()
497+
498+ assert len (configs ) == 2
499+ # First configuration should have bond_list
500+ assert 'bond_list' in configs [0 ]
501+ assert isinstance (configs [0 ]['bond_list' ], np .ndarray )
502+ assert configs [0 ]['bond_list' ].shape == (2 , 2 )
503+ # Second configuration should NOT have bond_list (topology is time-independent)
504+ assert 'bond_list' not in configs [1 ]
505+
506+
361507def test_get_thermodynamic_ensemble ():
362508 """Test ensemble determination from thermostat and barostat settings."""
363509 lp = gromacs_parser .GromacsLogParser ()
0 commit comments