Skip to content

Commit c2bbc63

Browse files
committed
Tests: TS IRC check
1 parent 6346db2 commit c2bbc63

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

arc/checks/ts_test.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from arc.level import Level
1818
from arc.parser.parser import parse_normal_mode_displacement, parse_geometry
1919
from arc.reaction import ARCReaction
20+
from arc.species.converter import xyz_from_data
2021
from arc.species.species import ARCSpecies, TSGuess
2122

2223

@@ -713,6 +714,145 @@ def test_check_imaginary_frequencies(self):
713714
imaginary_freqs = [-500.80, -3.14]
714715
self.assertTrue(ts.check_imaginary_frequencies(imaginary_freqs))
715716

717+
def test_perceive_irc_fragments_single_fragment(self):
718+
"""Test _perceive_irc_fragments for a single connected molecule (O=[C]COO)."""
719+
xyz_1 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_1.out'))
720+
frags = ts._perceive_irc_fragments(xyz_1, charge=0)
721+
self.assertIsNotNone(frags)
722+
self.assertEqual(len(frags), 1)
723+
self.assertTrue(frags[0].is_isomorphic(ARCSpecies(label='R', smiles='O=[C]COO').mol))
724+
725+
def test_perceive_irc_fragments_two_fragments(self):
726+
"""Test _perceive_irc_fragments for well-separated water + methane."""
727+
coords = (
728+
(0.0000, 0.0000, 0.1173), # O
729+
(0.0000, 0.7572, -0.4692), # H
730+
(0.0000, -0.7572, -0.4692), # H
731+
(10.0000, 0.0000, 0.0000), # C
732+
(10.6276, 0.6276, 0.6276), # H
733+
(10.6276, -0.6276, -0.6276), # H
734+
(9.3724, 0.6276, -0.6276), # H
735+
(9.3724, -0.6276, 0.6276), # H
736+
)
737+
symbols = ('O', 'H', 'H', 'C', 'H', 'H', 'H', 'H')
738+
xyz = xyz_from_data(coords=coords, symbols=symbols)
739+
frags = ts._perceive_irc_fragments(xyz, charge=0)
740+
self.assertIsNotNone(frags)
741+
self.assertEqual(len(frags), 2)
742+
water_mol = ARCSpecies(label='water', smiles='O').mol
743+
methane_mol = ARCSpecies(label='methane', smiles='C').mol
744+
# Fragment order follows atom indices: water (atoms 0-2) then methane (atoms 3-7)
745+
self.assertTrue(frags[0].is_isomorphic(water_mol))
746+
self.assertTrue(frags[1].is_isomorphic(methane_mol))
747+
748+
def test_perceive_irc_fragments_charge_handling(self):
749+
"""Test that single-fragment systems use the given charge, multi-fragment use charge=0."""
750+
# 1. Single fragment case: Ensure the +1 charge is passed through
751+
# Using the same XYZ but pretending it's a cation
752+
xyz_1 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_1.out'))
753+
frags_single = ts._perceive_irc_fragments(xyz_1, charge=1)
754+
755+
self.assertIsNotNone(frags_single)
756+
self.assertEqual(len(frags_single), 1)
757+
# Verify the perceived molecule actually inherited the +1 charge
758+
self.assertEqual(frags_single[0].get_net_charge(), 1)
759+
760+
# 2. Multi-fragment case: Ensure fragments are neutralized even if total charge is +1
761+
# We'll use a simple H2 + H2O mock XYZ to trigger the 2-fragment logic
762+
xyz_multi = {
763+
'symbols': ('H', 'H', 'O', 'H', 'H'),
764+
'coords': ((0, 0, 0), (0, 0, 0.74), # H2
765+
(5, 0, 0), (5.7, 0.7, 0), (5.7, -0.7, 0)) # H2O
766+
}
767+
frags_multi = ts._perceive_irc_fragments(xyz_multi, charge=1)
768+
769+
self.assertIsNotNone(frags_multi)
770+
self.assertEqual(len(frags_multi), 2)
771+
# Even though total system charge was 1, each fragment should be perceived as neutral (0)
772+
for frag in frags_multi:
773+
self.assertEqual(frag.get_net_charge(), 0)
774+
775+
def test_match_fragments_to_species_single(self):
776+
"""Test _match_fragments_to_species with a single fragment."""
777+
r_spc = ARCSpecies(label='R', smiles='O=[C]COO')
778+
p_spc = ARCSpecies(label='P', smiles='O=CCO[O]')
779+
xyz_1 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_1.out'))
780+
frags = ts._perceive_irc_fragments(xyz_1, charge=0)
781+
self.assertIsNotNone(frags)
782+
self.assertTrue(ts._match_fragments_to_species(frags, [r_spc.mol]))
783+
self.assertFalse(ts._match_fragments_to_species(frags, [p_spc.mol]))
784+
785+
def test_match_fragments_to_species_multi(self):
786+
"""Test _match_fragments_to_species with two well-separated fragments."""
787+
coords = (
788+
(0.0000, 0.0000, 0.1173),
789+
(0.0000, 0.7572, -0.4692),
790+
(0.0000, -0.7572, -0.4692),
791+
(10.0000, 0.0000, 0.0000),
792+
(10.6276, 0.6276, 0.6276),
793+
(10.6276, -0.6276, -0.6276),
794+
(9.3724, 0.6276, -0.6276),
795+
(9.3724, -0.6276, 0.6276),
796+
)
797+
symbols = ('O', 'H', 'H', 'C', 'H', 'H', 'H', 'H')
798+
xyz = xyz_from_data(coords=coords, symbols=symbols)
799+
frags = ts._perceive_irc_fragments(xyz, charge=0)
800+
self.assertIsNotNone(frags)
801+
802+
water_mol = ARCSpecies(label='water', smiles='O').mol
803+
methane_mol = ARCSpecies(label='methane', smiles='C').mol
804+
nh3_mol = ARCSpecies(label='NH3', smiles='N').mol
805+
806+
# Correct match (either order of expected species should work due to permutations)
807+
self.assertTrue(ts._match_fragments_to_species(frags, [water_mol, methane_mol]))
808+
self.assertTrue(ts._match_fragments_to_species(frags, [methane_mol, water_mol]))
809+
# Wrong species
810+
self.assertFalse(ts._match_fragments_to_species(frags, [water_mol, nh3_mol]))
811+
# Wrong count
812+
self.assertFalse(ts._match_fragments_to_species(frags, [water_mol]))
813+
self.assertFalse(ts._match_fragments_to_species(frags, [water_mol, methane_mol, nh3_mol]))
814+
815+
def test_match_fragments_to_species_empty(self):
816+
"""Test _match_fragments_to_species edge cases."""
817+
self.assertTrue(ts._match_fragments_to_species([], []))
818+
water_mol = ARCSpecies(label='water', smiles='O').mol
819+
self.assertFalse(ts._match_fragments_to_species([], [water_mol]))
820+
821+
def test_check_irc_isomorphism_path(self):
822+
"""Test that the full check_irc_species_and_rxn uses isomorphism when mol objects are available."""
823+
xyz_1 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_1.out'))
824+
xyz_2 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_2.out'))
825+
rxn = ARCReaction(r_species=[ARCSpecies(label='R', smiles='O=[C]COO', xyz=xyz_1)],
826+
p_species=[ARCSpecies(label='P', smiles='O=CCO[O]', xyz=xyz_2)])
827+
rxn.ts_species = ARCSpecies(label='TS', is_ts=True)
828+
# Both species have mol objects, so isomorphism path should be used
829+
self.assertIsNotNone(rxn.r_species[0].mol)
830+
self.assertIsNotNone(rxn.p_species[0].mol)
831+
ts.check_irc_species_and_rxn(xyz_1=xyz_1, xyz_2=xyz_2, rxn=rxn)
832+
self.assertTrue(rxn.ts_species.ts_checks['IRC'])
833+
834+
def test_check_irc_swapped_endpoints(self):
835+
"""Test that check_irc_species_and_rxn works when IRC endpoints are swapped."""
836+
xyz_1 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_1.out'))
837+
xyz_2 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_2.out'))
838+
rxn = ARCReaction(r_species=[ARCSpecies(label='R', smiles='O=[C]COO', xyz=xyz_1)],
839+
p_species=[ARCSpecies(label='P', smiles='O=CCO[O]', xyz=xyz_2)])
840+
rxn.ts_species = ARCSpecies(label='TS', is_ts=True)
841+
# Pass endpoints in reverse order (xyz_2 as first, xyz_1 as second)
842+
ts.check_irc_species_and_rxn(xyz_1=xyz_2, xyz_2=xyz_1, rxn=rxn)
843+
self.assertTrue(rxn.ts_species.ts_checks['IRC'])
844+
845+
def test_check_irc_wrong_species(self):
846+
"""Test that check_irc_species_and_rxn returns False for mismatched species."""
847+
xyz_1 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_1.out'))
848+
xyz_2 = parse_geometry(os.path.join(ARC_TESTING_PATH, 'irc', 'rxn_1_irc_2.out'))
849+
# Use wrong product species
850+
rxn = ARCReaction(r_species=[ARCSpecies(label='R', smiles='O=[C]COO', xyz=xyz_1)],
851+
p_species=[ARCSpecies(label='P_wrong', smiles='CC', xyz=xyz_2)])
852+
rxn.ts_species = ARCSpecies(label='TS', is_ts=True)
853+
ts.check_irc_species_and_rxn(xyz_1=xyz_1, xyz_2=xyz_2, rxn=rxn)
854+
self.assertFalse(rxn.ts_species.ts_checks['IRC'])
855+
716856
@classmethod
717857
def tearDownClass(cls):
718858
"""

0 commit comments

Comments
 (0)