Skip to content

Commit d8df5f4

Browse files
committed
Tests: xTB parser
1 parent bc6d872 commit d8df5f4

1 file changed

Lines changed: 87 additions & 10 deletions

File tree

arc/parser/parser_test.py

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -480,18 +480,18 @@ def test_parse_geometry_2(self):
480480
self.assertIsInstance(xyz_1, dict)
481481
self.assertEqual(len(xyz_1['symbols']), 53)
482482

483-
path_2 = os.path.join(ARC_TESTING_PATH, 'opt', 'xtb_opt_1.out') # Turbomol format
483+
path_2 = os.path.join(ARC_TESTING_PATH, 'opt', 'xtb_opt_1.out') # Turbomol format ($coord is in Bohr)
484484
xyz_2 = parser.parse_geometry(log_file_path=path_2)
485485
expected_xyz_2 = {'symbols': ('C', 'C', 'C', 'O', 'H', 'H', 'H', 'H'),
486486
'isotopes': (12, 12, 12, 16, 1, 1, 1, 1),
487-
'coords': ((-2.1908361683609, -0.0875055545944093, -0.712358508847116),
488-
(-0.170663821576948, -0.721009080780027, 0.6339514359292),
489-
(2.27150340995404, 0.539200343733434, 0.305747355748416),
490-
(4.16185183882216, 0.0456870795397582, 1.46958543963615),
491-
(-2.10906550336131, 1.39281538085596, -2.11159862590894),
492-
(-3.98822887666127, -1.01150815322938, -0.474092464462499),
493-
(-0.230883305078883, -2.20128348308961, 2.04098984698359),
494-
(2.25632242626311, 2.04360346756428, -1.15222446018155))}
487+
'coords': ((-1.1593401110647, -0.0463059268636, -0.3769637386362),
488+
(-0.0903113691106, -0.3815414223399, 0.3354725190107),
489+
(1.2020273599692, 0.2853324202958, 0.1617944684729),
490+
(2.2023562705124, 0.0241765516896, 0.7776708141903),
491+
(-1.1160689558722, 0.7370458647952, -1.1174094260626),
492+
(-2.110478992265, -0.5352668500015, -0.2508788280669),
493+
(-0.1221781347317, -1.1648685897309, 1.0800448842572),
494+
(1.1939939325626, 1.0814279521553, -0.6097306831655))}
495495
self.assertTrue(almost_equal_coords(xyz_2, expected_xyz_2))
496496

497497
path_3 = os.path.join(ARC_TESTING_PATH, 'opt', 'xtb_opt_2.out') # SDF format
@@ -732,10 +732,12 @@ def test_parse_1d_scan_energies(self):
732732
8.397974544321187, 7.071194090611243, 5.214457982623571, 3.4362612986915337,
733733
1.958199294316728, 0.8766536693692615, 0.22504856466548517,
734734
0.0004629911018128041])
735+
# Length-matched: 44 energies -> 44 angles (step = 360/(n+1) = 8 deg)
736+
self.assertEqual(len(angles_3), len(energies_3))
735737
self.assertEqual(angles_3, [0.0, 8.0, 16.0, 24.0, 32.0, 40.0, 48.0, 56.0, 64.0, 72.0, 80.0, 88.0, 96.0,
736738
104.0, 112.0, 120.0, 128.0, 136.0, 144.0, 152.0, 160.0, 168.0, 176.0, 184.0, 192.0,
737739
200.0, 208.0, 216.0, 224.0, 232.0, 240.0, 248.0, 256.0, 264.0, 272.0, 280.0, 288.0,
738-
296.0, 304.0, 312.0, 320.0, 328.0, 336.0, 344.0, 352.0])
740+
296.0, 304.0, 312.0, 320.0, 328.0, 336.0, 344.0])
739741

740742
path_4 = os.path.join(ARC_TESTING_PATH, 'rotor_scans', 'orca', 'cc.txt')
741743
energies_4, angles_4 = parser.parse_1d_scan_energies(log_file_path=path_4)
@@ -1000,6 +1002,81 @@ def test_parse_active_space(self):
10001002
xyz="""N 0.0 0.0 0.0"""))
10011003
self.assertEqual(active, {'e_o': (5, 4), 'occ': [3, 1, 1, 0, 1, 0, 0, 0], 'closed': [1, 0, 0, 0, 0, 0, 0, 0]})
10021004

1005+
# ----- xTB-specific improvements -----
1006+
1007+
def test_xtb_logfile_contains_errors(self):
1008+
"""xTB error detection should return None for clean files."""
1009+
from arc.parser.adapters.xtb import XTBParser
1010+
clean_path = os.path.join(ARC_TESTING_PATH, 'sp', 'NCC_xTB.out')
1011+
adapter = XTBParser(log_file_path=clean_path)
1012+
self.assertIsNone(adapter.logfile_contains_errors())
1013+
1014+
def test_xtb_parse_e_elect_avoids_false_match(self):
1015+
"""parse_e_elect should not match 'total energy gain' lines from optimization deltas.
1016+
1017+
The CO2_xtb.out file contains both 'total energy gain : -0.1724555 Eh' (delta)
1018+
and ':: total energy -10.308452243026 Eh' (final). The parser must return the
1019+
final value, not the delta.
1020+
"""
1021+
from arc.constants import E_h_kJmol
1022+
path = os.path.join(ARC_TESTING_PATH, 'freq', 'CO2_xtb.out')
1023+
e_elect = parser.parse_e_elect(path)
1024+
self.assertAlmostEqual(e_elect, -10.308452243026 * E_h_kJmol, places=3)
1025+
# Make sure we did NOT pick up the delta value
1026+
delta_value = -0.1724555 * E_h_kJmol # ~-452 kJ/mol
1027+
self.assertNotAlmostEqual(e_elect, delta_value, places=1)
1028+
1029+
def test_xtb_parse_frequencies_takes_last_block(self):
1030+
"""parse_frequencies should return the LAST eigval block.
1031+
1032+
TS_NH2+N2H3_xtb.out prints frequencies twice. Both blocks should be identical;
1033+
if the parser stops after the first block, it would still get the right values,
1034+
but using the last block is more robust against truncated/duplicate blocks.
1035+
"""
1036+
path = os.path.join(ARC_TESTING_PATH, 'freq', 'TS_NH2+N2H3_xtb.out')
1037+
freqs = parser.parse_frequencies(log_file_path=path)
1038+
self.assertIsNotNone(freqs)
1039+
# Imaginary frequency should be the first non-zero one
1040+
self.assertAlmostEqual(freqs[0], -781.89, places=1)
1041+
# Last frequency
1042+
self.assertAlmostEqual(freqs[-1], 3467.59, places=1)
1043+
# 24 modes total (3*8 - 6 = 18 + 6 zero = 24, but only 18 non-zero in TS)
1044+
self.assertEqual(len(freqs), 18)
1045+
1046+
def test_xtb_parse_zpe_correction_regex(self):
1047+
"""parse_zpe_correction uses regex to find ZPE in scientific notation safely."""
1048+
from arc.constants import E_h_kJmol
1049+
path = os.path.join(ARC_TESTING_PATH, 'freq', 'TS_NH2+N2H3_xtb.out')
1050+
zpe = parser.parse_zpe_correction(log_file_path=path)
1051+
self.assertIsNotNone(zpe)
1052+
self.assertAlmostEqual(zpe, 0.056690417480 * E_h_kJmol, places=3)
1053+
1054+
def test_xtb_parse_1d_scan_energies_length_match(self):
1055+
"""Energies and angles returned by parse_1d_scan_energies must have matching lengths."""
1056+
path = os.path.join(ARC_TESTING_PATH, 'rotor_scans', 'xtb_1', 'output.out')
1057+
energies, angles = parser.parse_1d_scan_energies(log_file_path=path)
1058+
self.assertIsNotNone(energies)
1059+
self.assertIsNotNone(angles)
1060+
self.assertEqual(len(energies), len(angles))
1061+
# First angle = 0, all angles strictly increasing
1062+
self.assertEqual(angles[0], 0.0)
1063+
for i in range(1, len(angles)):
1064+
self.assertGreater(angles[i], angles[i - 1])
1065+
# All angles should be in [0, 360)
1066+
self.assertLess(angles[-1], 360.0)
1067+
1068+
def test_xtb_parse_geometry_resets_on_new_block(self):
1069+
"""parse_geometry should not accumulate atoms across multiple geometry blocks.
1070+
1071+
For CO2_xtb.out (Turbomol $coord with 3 atoms), the parser should return
1072+
exactly 3 atoms, not 6 or more even if the file mentions multiple blocks.
1073+
"""
1074+
path = os.path.join(ARC_TESTING_PATH, 'freq', 'CO2_xtb.out')
1075+
xyz = parser.parse_geometry(log_file_path=path)
1076+
self.assertIsNotNone(xyz)
1077+
self.assertEqual(len(xyz['symbols']), 3)
1078+
self.assertEqual(xyz['symbols'], ('O', 'C', 'O'))
1079+
10031080

10041081
def test_parse_opt_steps(self):
10051082
"""Test parsing the number of optimization steps from various ESS output files."""

0 commit comments

Comments
 (0)