Skip to content

Commit df07063

Browse files
committed
filter_real_stderr_lines in arkane adapter
Remove clutter from ARC.log
1 parent 84cba77 commit df07063

4 files changed

Lines changed: 173 additions & 23 deletions

File tree

arc/output.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
AEC_SECTION_START, AEC_SECTION_END,
2525
MBAC_SECTION_START, MBAC_SECTION_END,
2626
PBAC_SECTION_START, PBAC_SECTION_END,
27+
filter_real_stderr_lines,
2728
find_best_across_files, get_qm_corrections_files,
2829
)
2930

@@ -339,8 +340,9 @@ def _get_energy_corrections(arkane_level_of_theory, bac_type: str | None) -> tup
339340
'fi"',
340341
]
341342
_, stderr = execute_command(command=commands, executable='/bin/bash')
342-
if stderr:
343-
logger.warning(f'get_qm_corrections.py stderr: {stderr}')
343+
real_stderr = filter_real_stderr_lines(stderr) if stderr else []
344+
if real_stderr:
345+
logger.warning(f'get_qm_corrections.py stderr: {real_stderr}')
344346

345347
result = read_yaml_file(tmp_out) or {}
346348
return result.get('aec'), result.get('bac')
@@ -412,8 +414,9 @@ def _compute_point_groups(species_dict: dict, project_directory: str) -> dict[st
412414
'fi"',
413415
]
414416
_, stderr = execute_command(command=commands, executable='/bin/bash')
415-
if stderr:
416-
logger.warning(f'get_point_groups.py stderr: {stderr}')
417+
real_stderr = filter_real_stderr_lines(stderr) if stderr else []
418+
if real_stderr:
419+
logger.warning(f'get_point_groups.py stderr: {real_stderr}')
417420

418421
result = read_yaml_file(tmp_out) or {}
419422
return {str(k): (str(v) if v is not None else None) for k, v in result.items()}

arc/processor.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from arc.imports import settings
1111
from arc.level import Level
1212
from arc.job.local import execute_command
13+
from arc.statmech.arkane import filter_real_stderr_lines
1314
from arc.statmech.factory import statmech_factory
1415

1516

@@ -298,8 +299,9 @@ def compare_thermo(species_for_thermo_lib: list,
298299
'fi"',
299300
]
300301
stdout, stderr = execute_command(command=commands, no_fail=True)
301-
if len(stderr):
302-
logger.error(f'Error while running RMG thermo script: {stderr}')
302+
real_stderr = filter_real_stderr_lines(stderr) if stderr else []
303+
if real_stderr:
304+
logger.error(f'Error while running RMG thermo script: {real_stderr}')
303305
species_list = read_yaml_file(path=species_thermo_path)
304306
for original_spc, rmg_spc in zip(species_for_thermo_lib, species_list):
305307
h298, s298, comment = rmg_spc.get('h298', None), rmg_spc.get('s298', None), rmg_spc.get('comment', None)

arc/statmech/arkane.py

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,60 @@
2929
RMG_ENV_NAME = settings.get('RMG_ENV_NAME', 'rmg_env')
3030
logger = get_logger()
3131

32+
# Substrings that indicate a stderr line is harmless shell-init / library
33+
# noise rather than a real subprocess failure. Any line containing one of
34+
# these substrings is dropped by ``filter_real_stderr_lines``. Add new
35+
# patterns here when a benign emitter trips us up.
36+
_STDERR_NOISE_SUBSTRINGS = (
37+
# Open Babel valence warnings around InChI generation.
38+
"Open Babel Warning",
39+
"Accepted unusual valence",
40+
"==============================",
41+
# JAX / TF startup chatter on some clusters.
42+
"pjrt_executable.cc",
43+
# Lmod (module system) "module load X" failures from the user's shell
44+
# init when the named module was renamed/removed on the cluster. These
45+
# don't break the spawned subprocess — Lmod just complains and moves on.
46+
"Lmod has detected",
47+
"module spider",
48+
"module --ignore_cache",
49+
"modulefiles written in TCL",
50+
"#%Module",
51+
"Please check the spelling or version number",
52+
"It is also possible your cache file is out-of-date",
53+
"Error while loading conda entry point",
54+
)
55+
56+
57+
def filter_real_stderr_lines(stderr):
58+
"""Drop harmless shell-init / library noise from a subprocess's stderr.
59+
60+
Accepts either a list of lines or a single multi-line string. Empty /
61+
whitespace-only lines and bare quoted module names (e.g. ``"openmpi"``
62+
on a Lmod-error line) are also dropped.
63+
64+
Returns a list of stripped lines that survived the filter — what's left
65+
is genuine error output the caller should surface.
66+
"""
67+
if isinstance(stderr, str):
68+
lines = stderr.splitlines()
69+
else:
70+
lines = list(stderr)
71+
real = []
72+
for raw in lines:
73+
line = raw.strip()
74+
if not line:
75+
continue
76+
if any(s in line for s in _STDERR_NOISE_SUBSTRINGS):
77+
continue
78+
# Lmod prints the offending module name on its own line as a bare
79+
# quoted token (e.g. `"openmpi"`). Drop those too.
80+
if (line.startswith('"') and line.endswith('"') and len(line) <= 64
81+
and ' ' not in line[1:-1]):
82+
continue
83+
real.append(line)
84+
return real
85+
3286
# Section boundary markers in the RMG quantum_corrections/data.py file.
3387
AEC_SECTION_START = "atom_energies = {"
3488
AEC_SECTION_END = "pbac = {"
@@ -505,8 +559,9 @@ def parse_arkane_thermo_output(self, statmech_dir: str) -> None:
505559
'fi"',
506560
]
507561
stdout, stderr = execute_command(command=commands, executable='/bin/bash')
508-
if len(stderr):
509-
logger.error(f'Error while running Arkane thermo script:\n{stderr}')
562+
real_stderr = filter_real_stderr_lines(stderr) if stderr else []
563+
if real_stderr:
564+
logger.error(f'Error while running Arkane thermo script:\n{real_stderr}')
510565
thermo_yaml_path = os.path.join(statmech_dir, 'thermo.yaml')
511566
if os.path.isfile(thermo_yaml_path):
512567
content = read_yaml_file(thermo_yaml_path) or {}
@@ -596,21 +651,7 @@ def run_arkane(statmech_dir: str) -> bool:
596651
no_fail=True,
597652
executable='/bin/bash')
598653
if std_err:
599-
ignorable_phrases = [
600-
"Open Babel Warning",
601-
"Accepted unusual valence",
602-
"==============================",
603-
"pjrt_executable.cc",
604-
]
605-
606-
real_errors = []
607-
for line in std_err:
608-
line = line.strip()
609-
if not line:
610-
continue
611-
if not any(phrase in line for phrase in ignorable_phrases):
612-
real_errors.append(line)
613-
654+
real_errors = filter_real_stderr_lines(std_err)
614655
if real_errors:
615656
logger.info(f'Arkane run failed with errors:\n{std_err}')
616657
return False

arc/statmech/arkane_test.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
_warn_no_match,
3434
check_arkane_aec,
3535
check_arkane_bacs,
36+
filter_real_stderr_lines,
3637
get_arkane_model_chemistry,
3738
)
3839
from unittest.mock import patch
@@ -869,5 +870,108 @@ def test_dh_rxn_uses_kJmol_e_elect_for_composite_species(self):
869870
self.assertAlmostEqual(rxn.dh_rxn298, -1e5, places=6)
870871

871872

873+
class TestFilterRealStderrLines(unittest.TestCase):
874+
"""``filter_real_stderr_lines`` strips harmless shell-init / library noise
875+
from a subprocess's stderr so ARC doesn't classify a successful Arkane run
876+
as a failure."""
877+
878+
def test_open_babel_unusual_valence_warning_is_noise(self):
879+
"""Existing carve-out: Open Babel's InChI-code warnings are harmless."""
880+
lines = [
881+
"==============================",
882+
"*** Open Babel Warning in InChI code",
883+
" #1 :Accepted unusual valence(s): C(2)",
884+
"==============================",
885+
]
886+
self.assertEqual(filter_real_stderr_lines(lines), [])
887+
888+
def test_lmod_unknown_module_warning_is_noise(self):
889+
"""Regression: a stale ``module load openmpi`` in shell init prints a
890+
Lmod block to stderr. ARC was treating it as a fatal Arkane failure
891+
even though Arkane completed successfully."""
892+
lines = [
893+
'Lmod has detected the following error: The following module(s) are unknown:',
894+
'"openmpi"',
895+
'',
896+
'Please check the spelling or version number. Also try "module spider ..."',
897+
'It is also possible your cache file is out-of-date; it may help to try:',
898+
' $ module --ignore_cache load "openmpi"',
899+
'',
900+
'Also make sure that all modulefiles written in TCL start with the string',
901+
'#%Module',
902+
]
903+
self.assertEqual(filter_real_stderr_lines(lines), [])
904+
905+
def test_conda_libmamba_solver_load_failure_is_noise(self):
906+
"""A broken `_sqlite3` in the base conda (missing `sqlite3_deserialize`)
907+
causes conda-libmamba-solver to fail loading and emit a stderr line on
908+
every `conda run` invocation. The subprocess itself succeeds — conda
909+
falls back to the classic solver — so this line is benign noise."""
910+
lines = [
911+
('Error while loading conda entry point: conda-libmamba-solver '
912+
'(/home/alon/miniconda3/lib/python3.11/lib-dynload/'
913+
'_sqlite3.cpython-311-x86_64-linux-gnu.so: undefined symbol: '
914+
'sqlite3_deserialize)'),
915+
] * 4
916+
self.assertEqual(filter_real_stderr_lines(lines), [])
917+
918+
def test_conda_entry_point_noise_does_not_mask_real_error(self):
919+
"""Conda-entry-point noise is filtered, but a real traceback emitted by
920+
the same script must still survive."""
921+
lines = [
922+
'Error while loading conda entry point: conda-libmamba-solver (...)',
923+
'Traceback (most recent call last):',
924+
'RuntimeError: thermo failed',
925+
]
926+
result = filter_real_stderr_lines(lines)
927+
self.assertEqual(
928+
result,
929+
['Traceback (most recent call last):', 'RuntimeError: thermo failed'],
930+
)
931+
932+
def test_real_error_is_preserved(self):
933+
"""A genuine traceback line passes through."""
934+
lines = [
935+
"==============================",
936+
"*** Open Babel Warning",
937+
"Traceback (most recent call last):",
938+
' File "arkane/main.py", line 123, in run',
939+
"RuntimeError: Could not parse output",
940+
]
941+
result = filter_real_stderr_lines(lines)
942+
self.assertIn("Traceback (most recent call last):", result)
943+
self.assertIn("RuntimeError: Could not parse output", result)
944+
self.assertNotIn("==============================", result)
945+
946+
def test_mixed_lmod_and_real_error_keeps_only_real(self):
947+
"""When Lmod noise and a real error coexist, only the real error
948+
survives. (The user's H/H2/OH composite SP failures should still
949+
surface even if the shell init is noisy.)"""
950+
lines = [
951+
'Lmod has detected the following error: ...',
952+
'"openmpi"',
953+
'#%Module',
954+
"AttributeError: 'NoneType' object has no attribute 'thermo'",
955+
]
956+
result = filter_real_stderr_lines(lines)
957+
self.assertEqual(
958+
result,
959+
["AttributeError: 'NoneType' object has no attribute 'thermo'"],
960+
)
961+
962+
def test_empty_and_whitespace_only_lines_dropped(self):
963+
self.assertEqual(filter_real_stderr_lines(["", " ", "\t"]), [])
964+
965+
def test_accepts_string_as_well_as_list(self):
966+
"""Some call sites pass a multiline string; the helper splits it."""
967+
s = (
968+
'Lmod has detected the following error: blah\n'
969+
'"openmpi"\n'
970+
'\n'
971+
'Genuine error: foo\n'
972+
)
973+
self.assertEqual(filter_real_stderr_lines(s), ["Genuine error: foo"])
974+
975+
872976
if __name__ == '__main__':
873977
unittest.main(testRunner=unittest.TextTestRunner(verbosity=2))

0 commit comments

Comments
 (0)