Skip to content

Commit 74fc4fa

Browse files
committed
B4
1 parent 260a9c7 commit 74fc4fa

10 files changed

Lines changed: 444 additions & 25 deletions

File tree

arc/job/adapters/molpro.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,21 @@ def write_input_file(self) -> None:
249249
if self.level.method in MRCC_ROUTED_METHODS:
250250
# Restriction is implicit from the preceding {hf;...} block; the
251251
# MRCC plugin call does not accept a 'u'/'r' prefix.
252-
input_dict['method'] = '{mrcc,method=' + self.level.method.upper() + '}'
252+
mrcc_call = '{mrcc,method=' + self.level.method.upper() + '}'
253+
if not is_restricted(self):
254+
# Open-shell wavefunction: Molpro emits ROHF for the {hf;...}
255+
# block above. MRCC's approximate-CC family (CCSDT, CCSDT(Q),
256+
# CCSDTQ, CCSDTQ(P)) refuses standard ROHF orbitals with the
257+
# error "Approximate CC methods are not implemented for
258+
# standard ROHF orbitals! Use semicanonical orbitals!".
259+
# Running {uccsd} on the ROHF reference produces semicanonical
260+
# orbitals as a side effect; MRCC then picks them up
261+
# automatically. This costs one extra UCCSD pass per sub-job
262+
# but is the only way the post-(T) MRCC methods will run for
263+
# radicals. {ccsd} on its own would be cheaper but does not
264+
# consistently produce SC orbitals across Molpro versions.
265+
mrcc_call = '{uccsd}\n' + mrcc_call
266+
input_dict['method'] = mrcc_call
253267
input_dict['restricted'] = ''
254268

255269
# Job type specific options

arc/job/adapters/molpro_test.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,13 +460,28 @@ def test_write_mrci_input_file(self):
460460
self.assertEqual(content_7, job_7_expected_input_file)
461461

462462
def test_write_input_file_mrcc_routing(self):
463-
"""Methods unsupported by native Molpro but supported by MRCC are routed through the MRCC plugin."""
463+
"""Methods unsupported by native Molpro but supported by MRCC are routed through the MRCC plugin.
464+
465+
For an open-shell wavefunction, an additional ``{uccsd}`` step is
466+
emitted between the HF block and the MRCC plugin call. Molpro defaults
467+
to ROHF for open-shell HF, but MRCC's approximate-CC family
468+
(``CCSDT``, ``CCSDT(Q)``, ``CCSDTQ``, ``CCSDTQ(P)``) refuses standard
469+
ROHF orbitals with the error::
470+
471+
Approximate CC methods are not implemented for standard ROHF orbitals!
472+
Use semicanonical orbitals!
473+
474+
Running ``{uccsd}`` on the ROHF reference produces the semicanonical
475+
orbitals MRCC needs as a side effect — they're left on disk and the
476+
subsequent ``{mrcc,method=...}`` call picks them up automatically.
477+
"""
464478
self.job_mrcc_ccsdt.cpu_cores = 48
465479
self.job_mrcc_ccsdt.set_input_file_memory()
466480
self.job_mrcc_ccsdt.write_input_file()
467481
with open(os.path.join(self.job_mrcc_ccsdt.local_path,
468482
input_filenames[self.job_mrcc_ccsdt.job_adapter]), 'r') as f:
469483
content_ccsdt = f.read()
484+
# spc1 has multiplicity=3 (open-shell triplet) — uccsd prefix expected.
470485
expected_ccsdt = """***,spc1
471486
memory,Total=438,m;
472487
@@ -486,6 +501,7 @@ def test_write_input_file_mrcc_routing(self):
486501
wf,spin=2,charge=0;
487502
}
488503
504+
{uccsd}
489505
{mrcc,method=CCSDT}
490506
491507
@@ -497,6 +513,13 @@ def test_write_input_file_mrcc_routing(self):
497513
# Sanity: the bare directive Molpro rejects must NOT appear on its own line.
498514
self.assertNotIn('\nccsdt;\n', content_ccsdt)
499515
self.assertNotIn('\nuccsdt;\n', content_ccsdt)
516+
# The uccsd step must come between the HF block and the mrcc plugin call —
517+
# not after, not before HF.
518+
uccsd_idx = content_ccsdt.index('{uccsd}')
519+
hf_close_idx = content_ccsdt.index('}\n\n{uccsd}')
520+
mrcc_idx = content_ccsdt.index('{mrcc,method=CCSDT}')
521+
self.assertLess(hf_close_idx, uccsd_idx)
522+
self.assertLess(uccsd_idx, mrcc_idx)
500523

501524
self.job_mrcc_ccsdtq.cpu_cores = 48
502525
self.job_mrcc_ccsdtq.set_input_file_memory()
@@ -532,6 +555,9 @@ def test_write_input_file_mrcc_routing(self):
532555
"""
533556
self.assertEqual(content_ccsdtq, expected_ccsdtq)
534557
self.assertNotIn('\nccsdt(q);\n', content_ccsdtq)
558+
# spc1 here has multiplicity=1 (closed-shell) — no semicanonical-orbital
559+
# prep step is needed and none should be emitted.
560+
self.assertNotIn('{uccsd}', content_ccsdtq)
535561

536562
def test_set_files(self):
537563
"""Test setting files"""

arc/job/trsh.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,10 +395,41 @@ def determine_ess_status(output_path: str,
395395
return 'errored', keywords, error, line
396396

397397
elif software == 'molpro':
398+
# MRCC ROHF-incompatibility check BEFORE the generic reverse scan
399+
# because the underlying cause ("Use semicanonical orbitals!")
400+
# appears earlier in the file than the downstream "Fatal error in
401+
# mrcc." line — reverse iteration would otherwise classify the
402+
# latter (generic) before the former (specific). Fix in the
403+
# adapter prepends ``{uccsd}`` to generate semicanonical orbitals;
404+
# this keyword surfaces the diagnostic for any legacy run that
405+
# hits it.
406+
joined = '\n'.join(lines) if isinstance(lines, list) else str(lines)
407+
if 'standard ROHF orbitals' in joined or 'Use semicanonical orbitals' in joined:
408+
rohf_line = next(
409+
(ln for ln in lines if 'standard ROHF orbitals' in ln
410+
or 'Use semicanonical orbitals' in ln),
411+
'',
412+
)
413+
return ('errored', ['MRCCRequiresSemicanonical'],
414+
'MRCC requires semicanonical orbitals; ROHF orbitals '
415+
'are not supported for approximate CC.',
416+
rohf_line)
398417
for line in reverse_lines:
399418
if 'molpro calculation terminated' in line.lower() \
400419
or 'variable memory released' in line.lower():
401420
return 'done', list(), '', ''
421+
elif 'Fatal error in xmrcc' in line or 'Fatal error in mrcc' in line:
422+
# MRCC bailed for a tiny system where the requested CC
423+
# excitation rank exceeds the determinant space (e.g.
424+
# atomic H or H2 at CCSDT(Q)). The composite framework
425+
# should short-circuit a δ-term high leg with this
426+
# keyword to the corresponding low-leg energy (δ = 0,
427+
# which is correct for a degenerate-method case).
428+
keywords = ['MRCCDegenerateSystem']
429+
error = ('MRCC xmrcc fatal — the requested CC excitation '
430+
'rank exceeds the determinant space for this '
431+
'system (degenerate / too few electrons).')
432+
break
402433
elif 'No convergence' in line and '?No convergence in rhfpr' not in line:
403434
keywords = ['Unconverged']
404435
error = 'Unconverged'

arc/job/trsh_test.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,32 @@ def test_determine_ess_status(self):
171171
self.assertEqual(error, "Unrecognized basis set 6-311G**")
172172
self.assertIn(" ? Basis library exhausted", line) # line includes '\n'
173173

174+
# Molpro + MRCC: degenerate small system (e.g. atomic H, H2 at CCSDT(Q)).
175+
# MRCC's xmrcc bails because there's no determinant space at the
176+
# requested excitation rank. Trsh must classify this so the framework
177+
# knows to short-circuit the sub-job (delta = 0) instead of cycling
178+
# the generic ladder (shift / vdz / memory).
179+
path = os.path.join(self.base_path["molpro"], "mrcc_xmrcc_fatal.out")
180+
status, keywords, error, line = trsh.determine_ess_status(
181+
output_path=path, species_label="H", job_type="sp"
182+
)
183+
self.assertEqual(status, "errored")
184+
self.assertEqual(keywords, ["MRCCDegenerateSystem"])
185+
self.assertIn("xmrcc", error.lower())
186+
self.assertIn("Fatal error in xmrcc", line)
187+
188+
# Molpro + MRCC: ROHF orbitals incompatible with approximate CC methods
189+
# (open-shell radicals). Trsh classifies and the adapter's UCCSD
190+
# prefix should prevent this from happening on new runs; the keyword
191+
# is the diagnostic for any legacy runs that don't have the prefix.
192+
path = os.path.join(self.base_path["molpro"], "mrcc_rohf_unsupported.out")
193+
status, keywords, error, line = trsh.determine_ess_status(
194+
output_path=path, species_label="OH", job_type="sp"
195+
)
196+
self.assertEqual(status, "errored")
197+
self.assertEqual(keywords, ["MRCCRequiresSemicanonical"])
198+
self.assertIn("semicanonical", error.lower())
199+
174200
# Orca
175201

176202
# test detection of a successful job

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: Optional[str]) ->
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
@@ -11,6 +11,7 @@
1111
from arc.imports import settings
1212
from arc.level import Level
1313
from arc.job.local import execute_command
14+
from arc.statmech.arkane import filter_real_stderr_lines
1415
from arc.statmech.factory import statmech_factory
1516

1617

@@ -271,8 +272,9 @@ def compare_thermo(species_for_thermo_lib: list,
271272
'fi"',
272273
]
273274
stdout, stderr = execute_command(command=commands, no_fail=True)
274-
if len(stderr):
275-
logger.error(f'Error while running RMG thermo script: {stderr}')
275+
real_stderr = filter_real_stderr_lines(stderr) if stderr else []
276+
if real_stderr:
277+
logger.error(f'Error while running RMG thermo script: {real_stderr}')
276278
species_list = read_yaml_file(path=species_thermo_path)
277279
for original_spc, rmg_spc in zip(species_for_thermo_lib, species_list):
278280
h298, s298, comment = rmg_spc.get('h298', None), rmg_spc.get('s298', None), rmg_spc.get('comment', None)

arc/scheduler.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,75 @@ def _post_sp_actions_composite(
17311731
else:
17321732
self._finalize_composite(label)
17331733

1734+
def _mrcc_degenerate_short_circuit(self, label: str, job) -> bool:
1735+
"""If a composite δ-term high-leg sub-job errored with the
1736+
``MRCCDegenerateSystem`` keyword, substitute the sister low-leg's
1737+
recorded path and advance the composite. Returns ``True`` when the
1738+
short-circuit was applied so the caller can skip ``troubleshoot_ess``.
1739+
1740+
Why this exists: for systems too small to accommodate the requested
1741+
CC excitation rank (atomic H, H2 at CCSDT(Q), etc.) MRCC's xmrcc
1742+
bails — there's no determinant space to iterate. The two legs of
1743+
the δ term are mathematically degenerate (all CC ranks reduce to
1744+
HF), so δ = 0 is the correct physical answer. The trsh ladder
1745+
cannot fix this; it must be handled at the protocol level.
1746+
1747+
Conservative on safety:
1748+
* Only fires when keywords contain ``'MRCCDegenerateSystem'``.
1749+
* Only fires when the species has an active composite.
1750+
* Only fires for ``__high`` sub-labels with a recorded ``__low``
1751+
sister in ``output[label]['paths']['sp_composite']``. Low-leg
1752+
failures and high-leg failures without a completed sister fall
1753+
through so the caller's normal trsh path runs.
1754+
"""
1755+
keywords = (job.job_status[1] or {}).get('keywords') or []
1756+
if 'MRCCDegenerateSystem' not in keywords:
1757+
return False
1758+
protocol = self._composite_for(label)
1759+
if protocol is None:
1760+
return False
1761+
# Identify which pending sub_label this errored job corresponds to.
1762+
pending = self._sp_composite_pending.get(label, {})
1763+
failed_sub_label = next(
1764+
(sl for sl, lvl in pending.items() if lvl == job.level),
1765+
None,
1766+
)
1767+
if failed_sub_label is None or not failed_sub_label.endswith('__high'):
1768+
logger.warning(format_log_event(
1769+
label,
1770+
"MRCCDegenerateSystem on a non-high sub-job — falling through to trsh",
1771+
{"sub_label": failed_sub_label, "level": str(job.level)},
1772+
))
1773+
return False
1774+
sister_low = failed_sub_label[: -len('__high')] + '__low'
1775+
completed = self.output.get(label, {}).get('paths', {}).get('sp_composite', {}) or {}
1776+
sister_path = completed.get(sister_low)
1777+
if not sister_path:
1778+
logger.warning(format_log_event(
1779+
label,
1780+
"MRCCDegenerateSystem high-leg failed but low-leg not yet recorded — deferring",
1781+
{"failed": failed_sub_label, "sister": sister_low},
1782+
))
1783+
return False
1784+
logger.warning(format_log_event(
1785+
label,
1786+
"MRCC degenerate-system fallback: substituting low-leg energy for high-leg",
1787+
{"failed": failed_sub_label, "sister": sister_low,
1788+
"reason": "Method exceeds determinant space; δ = 0 by symmetry."},
1789+
))
1790+
# Substitute by re-using the low-leg's path. _record_composite_completion
1791+
# writes the path into output and clears the pending entry — same as a
1792+
# normal completion, but pointing both legs at the same file so they
1793+
# parse to the same energy and the δ evaluates to zero.
1794+
completed_paths = self.output[label]['paths']['sp_composite']
1795+
completed_paths[failed_sub_label] = sister_path
1796+
del pending[failed_sub_label]
1797+
if pending:
1798+
self._spawn_composite_pending(label)
1799+
else:
1800+
self._finalize_composite(label)
1801+
return True
1802+
17341803
def _rehydrate_composite_state(self) -> None:
17351804
"""On scheduler init/restart, seed pending for every species with an active
17361805
composite, rebuild the SpeciesSection for species that already finalized,
@@ -3212,6 +3281,13 @@ def check_sp_job(self,
32123281
if self.species_dict[label].number_of_atoms == 1:
32133282
# save the geometry from the sp job for monoatomic species for which no opt/freq jobs will be spawned
32143283
self.output[label]['paths']['geo'] = job.local_path_to_output_file
3284+
elif self._mrcc_degenerate_short_circuit(label=label, job=job):
3285+
# MRCC bailed for a degenerate small-system δ-term high leg
3286+
# (atomic H, H2, etc., where the requested CC excitation rank
3287+
# exceeds the determinant space). The trsh ladder cannot help —
3288+
# cause is intrinsic. Helper substituted the low-leg energy and
3289+
# advanced the composite; nothing further to do here.
3290+
self.save_restart_dict()
32153291
else:
32163292
self.troubleshoot_ess(label=label,
32173293
job=job,

0 commit comments

Comments
 (0)