Skip to content

NMD check rejects a likely-correct late H_Abstraction TS, and deletes the IRC jobs that would adjudicate it #1002

Description

@alongd

Summary

The normal mode displacement (NMD) check rejects what appears to be a correct H_Abstraction
saddle point, and because the rejection happens after the TS has already converged, ARC deletes
the IRC and single-point jobs it had queued for that TS and abandons the reaction. With only one
TS guess available there is nothing to switch to, so the run ends with
Could not determine a likely TS conformer and no rate coefficient.

The IRC jobs are exactly what would have adjudicated the NMD verdict, so the current ordering
removes the evidence needed to check the gate that fired.

Reaction: H + HOCHO <=> H2 + OCHO (O–H abstraction from formic acid, giving formyloxy)
ARC: d033cbbfd4c014d6b07c8efb6abc8bccfe40efd2 (main, 2026-08-18)
Levels: opt/freq b2plypd3/def2tzvp (Gaussian 16), sp ccsd(t)-f12/aug-cc-pvtz-f12 (Molpro)
TS adapters: ['heuristics'] (one guess returned, success=True)

The sibling C–H channel H + HOCHO <=> H2 + HOCO, run identically in a parallel project, passes
the NMD check without trouble — so this is channel-specific rather than a broken setup.

What ARC logged

TS TS0 has exactly one imaginary frequency: -923.6275
TS TS0 did not pass the normal mode displacement check. Status is:
{'E0': None, 'e_elect': None, 'IRC': None, 'freq': True, 'NMD': False, 'warnings': ''}
Searching for a better TS conformer...
Switching a TS guess for TS0...
Warning: Could not determine a likely TS conformer for TS0
Deleted job irc_a3438
Deleted job irc_a3439
Deleted job sp_a3440
Error: Species TS0 did not converge. Job type status is:
{'fine': False, 'freq': False, 'irc': False, 'opt': False, 'rotors': True, 'sp': False}
Error: Cannot compute a rate coefficient for H + HOCHO <=> H2 + OCHO. TS ['TS0'] did not converge.

Why the saddle looks correct

Converged geometry (b2plypd3/def2tzvp, Å):

H       0.02119200   -2.20112700    0.00000000
O       1.17181500    0.21772300    0.00000000
C      -0.00000000    0.47936600   -0.00000000
O      -1.05950200   -0.29318100   -0.00000000
H      -0.40019200    1.51881100   -0.00000000
H      -0.51950300   -1.59021200    0.00000000

Connectivity is the intended one: the attacking H0 approaches the hydroxyl H5 (H0–H5 = 0.816 Å),
the O3–H5 bond being broken is stretched to 1.405 Å, and the formyl C2–H4 bond is untouched at
1.114 Å. So it is the O–H channel and not the C–H one.

Imaginary mode displacement, the single imaginary frequency at −923.63 cm⁻¹:

atom displacement |d|
0 H (−0.21, +0.29, 0.00) 0.36
1 O (−0.01, −0.02, 0.00) 0.02
2 C (−0.01, +0.04, 0.00) 0.04
3 O (+0.04, −0.09, 0.00) 0.10
4 H (+0.08, +0.03, 0.00) 0.09
5 H (−0.23, +0.90, 0.00) 0.93

The mode is dominated by the transferring hydrogen H5. Its displacement direction, normalised, is
(−0.25, +0.97); the H5→O3 unit vector is (−0.39, +0.92). Those are parallel to within a dot
product of 0.99, i.e. H5 is moving along the O3···H5···H0 axis — the reaction coordinate. Every
heavy atom is nearly stationary (|d| ≤ 0.10).

For comparison, the C–H channel's accepted saddle has its imaginary mode at −1567.75 cm⁻¹
dominated by the transferring H4 with |d| = 0.98, i.e. the same signature.

All 12 modes (cm⁻¹): −923.6, 232.2, 319.2, 647.5, 649.2, 838.9, 987.7, 1041.2, 1300.3, 1752.1,
2699.7, 2871.6 — one imaginary, eleven real, none spurious.

Suspected cause

This is a late saddle, which is what Hammond's postulate predicts here: O–H abstraction from
formic acid by an H atom is endothermic by roughly 8 kcal/mol, whereas the C–H channel is
exothermic by about the same. Hence H–H already at 0.816 Å (vs 0.741 Å in free H₂) and O–H
stretched to 1.405 Å.

My guess is that the NMD criterion is calibrated on displacement along the breaking and
forming bonds in a more central saddle, and that a product-like geometry — where the forming
H–H is nearly complete and the transferring atom's motion projects mostly onto the bond being
re-formed rather than symmetrically onto both — falls outside the accepted window. I have not read
the NMD implementation closely enough to point at a line, so treat that as a hypothesis rather
than a diagnosis.

The atom map for this reaction is [0, 2, 3, 4, 1, 5], and an independent check confirms it maps
the hydroxyl H (not the formyl H), so a mis-mapping is not the cause.

Impact

  1. A likely-correct TS is discarded, and with a single TS guess the whole reaction is abandoned.
  2. The IRC jobs are deleted on the NMD verdict. The IRC is the authoritative test of whether a
    saddle connects the intended pair; deleting it removes the only evidence that could confirm or
    refute the NMD result. Running the IRC to completion before acting on NMD — or at least not
    cancelling already-queued IRC jobs — would make the failure diagnosable from the run itself.
  3. The Deleted job … lines are info-level, so from a normal log it is not obvious that
    diagnostic work was cancelled rather than never scheduled.

Suggested handling

  • Let queued IRC jobs finish and treat IRC connectivity as authoritative when it disagrees with NMD.
  • Emit the NMD verdict with the numbers it was computed from (which displacements, against which
    expected bonds, and the threshold), so a user can tell a genuine miss from a borderline call.
  • Possibly relax or re-derive the criterion for strongly asymmetric (very early / very late)
    saddles.

Workaround in the meantime is skip_nmd: true, which works, but it disables the check globally
rather than for the one reaction where it misfires.

Related, separate bug (happy to file on its own if preferred)

The same run later crashed outright in the change-node troubleshooting path:

File "arc/job/trsh.py", line 1489, in trsh_job_on_server
    nodes = ssh.list_available_nodes()
File "arc/job/ssh.py", line 454, in list_available_nodes
    stdout = self._send_command_to_server(command=cmd)[0]
File "arc/job/ssh.py", line 42, in decorator
    self._sftp, self._ssh = self.connect()
TypeError: cannot unpack non-iterable NoneType object

SSHClient.connect() is annotated -> None, assigns self._sftp and self._ssh itself, and
returns bare — but check_connections unpacks its return value into those same two attributes. It
is rarely reached because SSHClient is normally used as a context manager (__enter__ populates
_ssh); the change-node path constructs a bare client and calls list_available_nodes() directly,
so self._ssh is None and the buggy branch runs. Calling self.connect() without unpacking fixes
it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions