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
- A likely-correct TS is discarded, and with a single TS guess the whole reaction is abandoned.
- 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.
- 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.
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 conformerand 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), spccsd(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, passesthe NMD check without trouble — so this is channel-specific rather than a broken setup.
What ARC logged
Why the saddle looks correct
Converged geometry (
b2plypd3/def2tzvp, Å):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⁻¹:
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 mapsthe hydroxyl H (not the formyl H), so a mis-mapping is not the cause.
Impact
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.
Deleted job …lines areinfo-level, so from a normal log it is not obvious thatdiagnostic work was cancelled rather than never scheduled.
Suggested handling
expected bonds, and the threshold), so a user can tell a genuine miss from a borderline call.
saddles.
Workaround in the meantime is
skip_nmd: true, which works, but it disables the check globallyrather 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:
SSHClient.connect()is annotated-> None, assignsself._sftpandself._sshitself, andreturns bare — but
check_connectionsunpacks its return value into those same two attributes. Itis rarely reached because
SSHClientis normally used as a context manager (__enter__populates_ssh); the change-node path constructs a bare client and callslist_available_nodes()directly,so
self._ssh is Noneand the buggy branch runs. Callingself.connect()without unpacking fixesit.