Test whether the SCF reference is the ground state, and act on the answer - #1014
Test whether the SCF reference is the ground state, and act on the answer#1014calvinp0 wants to merge 12 commits into
Conversation
32f6064 to
ff61563
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1014 +/- ##
==========================================
+ Coverage 64.80% 65.28% +0.48%
==========================================
Files 119 120 +1
Lines 39997 40731 +734
Branches 10338 10519 +181
==========================================
+ Hits 25920 26592 +672
- Misses 11094 11123 +29
- Partials 2983 3016 +33
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
3eb7956 to
1c9636b
Compare
|
Reviewed at First, the honest framing: this is careful work. The instrumentation is good, the adoption contract is stated precisely enough to be tested against, the fixtures are real ESS output rather than hand-made, and the PR description is unusually self-critical. Most of what follows is downstream of a single line that predates this branch, and which this PR's own docstring already explains. I've measured that one, so it isn't a hypothesis. How to read the confidence numbers — they record how a claim was established, not how strongly it reads:
Fix first1. The adopted unrestricted reference collapses back to restricted — the feature is currently a no-op
Composed inputs, with an adopted verdict on a singlet TS: The line is pre-existing at the merge-base — this PR doesn't introduce it, it collides with it: And the collision is guaranteed on exactly the path that needs the other branch: You already state the mechanism, at
That reasoning is right, and it's applied to protect the measurement. It isn't applied to the jobs the verdict is adopted for. The measurementPySCF 2.14, B3LYP/def2-TZVP, grids level 4, Three things this establishes:
The internal/external split agrees across both programs: PySCF's Consequence. On this fixture, the adoption path produces −196.4901 Ha and records Caveat on provenance, stated plainly: this was measured in PySCF, not Gaussian. The mechanism is program-independent — the RKS solution is a stationary point of the UKS equations and α=β is preserved by the SCF — and the reference energy cross-validates against your own fixture. But PySCF's Reproduce itimport numpy as np
from pyscf import dft, gto
GEOM = """
C 0.42653500 1.47789200 -0.00007700
C 0.49427800 0.01191500 -0.00000300
C 1.79225900 -0.71265700 0.00003600
C -0.76952500 -0.70318200 0.00003500
C -2.09552400 -0.11756100 0.00001100
H 1.39854400 1.97008900 -0.00010900
H -0.16267200 1.83051900 -0.86690400
H 2.40600800 -0.45644800 -0.87511100
H 2.40600000 -0.45636800 0.87516500
H 1.65286800 -1.79552300 0.00008500
H -0.70100100 -1.41388300 0.84851300
H -0.70100600 -1.41396700 -0.84837200
H -2.27706200 0.94604900 -0.00003500
H -0.16265800 1.83060800 0.86672200
H -2.94716600 -0.77952000 0.00003900
""" # arc/testing/stability/rhf_uhf_instability_singlet_ts.out, first Input orientation block
mol = gto.M(atom=GEOM, basis='def2-tzvp', charge=0, spin=0, verbose=0, max_memory=16000)
mf_r = dft.RKS(mol); mf_r.xc = 'b3lyp'; mf_r.grids.level = 4
mf_r.conv_tol = 1e-9; mf_r.max_cycle = 200
e_r = mf_r.kernel()
def make_uks():
mf = dft.UKS(mol); mf.xc = 'b3lyp'; mf.grids.level = 4
mf.conv_tol = 1e-9; mf.max_cycle = 200
return mf
# guess=read analogue: seed UKS with the converged restricted density
dm_r = mf_r.make_rdm1()
mf_u1 = make_uks()
e_u1 = mf_u1.kernel(dm0=np.array([dm_r / 2, dm_r / 2]))
# guess=mix analogue: rotate HOMO/LUMO oppositely in the two spin channels
nocc = mol.nelectron // 2
h, l = nocc - 1, nocc
c = 1.0 / np.sqrt(2.0)
homo, lumo = mf_r.mo_coeff[:, h].copy(), mf_r.mo_coeff[:, l].copy()
mo_a, mo_b = mf_r.mo_coeff.copy(), mf_r.mo_coeff.copy()
mo_a[:, h], mo_a[:, l] = c * (homo + lumo), c * (homo - lumo)
mo_b[:, h], mo_b[:, l] = c * (homo - lumo), c * (homo + lumo)
occ = np.zeros(mf_r.mo_coeff.shape[1]); occ[:nocc] = 1.0
mf_u2 = make_uks()
e_u2 = mf_u2.kernel(dm0=mf_u2.make_rdm1((mo_a, mo_b), (occ, occ)))
print(e_r, e_u1, mf_u1.spin_square()[0], e_u2, mf_u2.spin_square()[0])Fix directionOn adoption emit Worth noting this is not reliably a uniform no-op: an unstable RKS solution is a saddle in orbital space, so DFT grid noise can seed the collapse in some species and not others. One campaign can yield a mix of restricted and broken-symmetry TSs, all labelled unrestricted. And it is invisible to the instrument this PR ships — 2. Five of the new tests are order-dependent and will flake CI red
logging.addLevelName(logging.WARNING, 'Warning: ') # process-global, permanentThe five new tests assert
This is also why the "zero failures" claim doesn't hold: full tree, single process, clean HEAD is Fix: assert 3. Adoption is inert for the species it measures
if not switch_ts and species_has_sp(...):
self.check_rxn_e0_by_spc(label) # 3182
if not switch_ts and (self.species_dict[label].is_ts
or job_scf_reference_is_restricted(job) is True):
self.run_stability_job(label=label, freq_job=job) # 3186The barrier is computed and E0-checked before the stability job is spawned. The verdict lands asynchronously, and Two independent passes reached this from opposite ends: the ordering above, and the observation that a TS guess's sp is typically already composed by the time its verdict lands. Worth deciding explicitly whether adoption is meant to be retroactive. If yes, it needs a re-run path. If no, the description's "adopted for subsequent jobs" should name which subsequent jobs actually exist. Then4. A DFT-level verdict flips the reference at every other level and ESS
The stability job runs at Neither Orca nor Molpro has symmetry-breaking machinery in ARC's templates (Orca needs 5. The mixed-reference check is structurally blind in the most common configuration
When Fix: record the reference in 6.
|
| Item | Location | Confidence |
|---|---|---|
| The six new fixtures carry the submitting account, hostname, compute-node name and scratch-path layout in their headers and archive blocks | arc/testing/{stability,spin}/*.out |
10 |
Unguarded int() on a log-derived digit run; qchem.py already guards the same thing |
gaussian.py:361, orca.py:226 |
10 |
parse_wavefunction_stability has no base-class default on ESSAdapter, unlike parse_s_squared/parse_ess_version; safe only via the getattr in make_parser |
arc/parser/adapter.py |
9 |
Returns: <type> inline instead of Google style |
spin.py:23,90,143; parser.py:294; job/adapters/common.py:363; output.py:267,296 |
9 |
No Returns: section at all |
run_stability_job, check_stability_job, log_open_shell_character_sources |
9 |
Two import tempfile inside test bodies |
parser_test.py:1258,1475 |
10 |
parse_s_squared implemented three times with a common skeleton |
gaussian/orca/qchem | 8 |
On the fixtures: this is the repo's existing norm rather than anything new — 13 tracked files at the merge-base carry the same host identifier and 6 the same account, and /gtmp is hardcoded in arc/settings/settings.py. No keys, tokens, emails or licence numbers are present. Scrubbing the six new ones is cheap and none of the new parsers key off those substrings; whether to backfill the pre-existing files is a separate question.
Checked and sound — please don't spend time re-verifying these
Chemistry
- The Yamaguchi formula is correct. Denominator is ⟨S²⟩_HS − ⟨S²⟩_BS, derived from E_BS = (1−w)E_LS + wE_HS with w = (S²_BS−S²_LS)/(S²_HS−S²_LS). Verified algebraically and numerically. The
s2_lsplumbing is complete — it reaches the formula, the ordering guard (:107) andbroken_symmetry(:150). - Before-annihilation ⟨S²⟩ is right and consistently fed.
s_squared: 0.7536from the<Sx>=line vss_squared_annihilated: 0.75. TheInitial guessexclusion works (uhf_died_before_scf_septet.out→None, not 12.0) and the first-Multiplicityrule survivesguess=fragment(→ expected 0.75, not the fragment's 1). - AEC/BAC is not a new problem.
arc/statmech/arkane.py:373-398applies corrections uniformly; a TS's energy enters only via E0(TS) − E0(reactants), where atom counts are identical and AEC cancels exactly. Wells are never adopted. Your stated reason for excluding wells is sound, and the AEC half doesn't additionally condemn the TS case. - The composite/semiempirical early return should be closed, not carried as a follow-up.
uCBS-QB3isn't a Gaussian keyword —CBS-QB3selects its open-shell variant from the multiplicity itself, as do AM1/PM6 — andrun_stability_job:1802-1806refuses any non-DFT/HF level, so a derived verdict can't exist for a composite species anyway. - Your
guess=mixre-derivation holds.arc/testing/restart/1_restart_thermo/calcs/freq_a19031.out: route line 84#P guess=mix wb97xd/def2tzvp freq, line 105Multiplicity = 1, line 223Mixing orbitals, IMix= 1 ... Coef= 7.07106781D-01 7.07106781D-01, line 422SCF Done: E(RwB97XD). g16 does apply mixing on a restricted reference. Note this is also the fact finding 1 turns on. - The fixtures are genuine — g16 RevC.02, real
l1.exepaths, real archive blocks, real timestamps.
Persistence
- The provenance does survive a restart — via
restart.yml/ARCSpecies.as_dict()(species.py:810-813), read back at:947-949. Not viaoutput.yml, which is confirmed write-only/export. The claim holds; the description's stated mechanism is the part to correct. - Old pre-PR restart fixtures resume cleanly. All 4 species of
1_restart_thermo/restart.ymlconstructed; the three new attributes defaulted toNone/{}/None. NoKeyError/TypeError/AttributeError. - No stability respawn on resumed old projects —
default_job_types['stability']isFalseandinitialize_job_typesfills the missing key fromdefaults_to_false. - The
number_of_radicalsprovenance question is moot. Only two assignments exist inarc/: the constructor default (species.py:382) and the restart read (:946). ARC never writes it programmatically, so a restored value is exactly as user-declared as a fresh one. - No tuple→list degradation. In real usage
relaxations/negative_eigenvectorsare always lists. ts_checkssemantics untouched by this diff.restricted_usedround-trips safely — popped inrestore_running_jobsbeforejob_factory.
Contracts
ESSAdapter.parse_s_squaredhas a safereturn Nonedefault (arc/parser/adapter.py:210), not@abstractmethod. All 9 subclasses probed; the 6 without an override inherit cleanly, noAttributeError.- No
JobAdapterabstract method added, so no adapter is forced to implement anything new.job_type='stability'is only ever constructed byrun_stability_job, which gates onjob_adapter != 'gaussian'. - The
default_job_typesin-place-mutation trap does not bite —arc/common.py:92listsstabilityindefaults_to_false; zero import-timedefault_job_types[key]subscripts anywhere. specific_job_type: stabilityruns nothing, confirmed by execution, matching your own admission — and it does not also hang, sincecheck_all_done(:3843-3845) skipsstability. It's the only such trap this PR introduces.check_negative_freq's return-shape change is fully absorbed — both call sites unpack, all 9 returns in the body are 2-tuples.
Security / performance
- No ReDoS in any of the nine new regexes at n = 200 / 2,000 / 20,000 / 200,000 — nothing over 50 ms. Both parsers gate on cheap substring checks before invoking
re.search. - No
eval/exec/pickle/yaml.load/subprocess/shell=Truein the new source. - No log-derived string reaches an input deck. The stability route section (
gaussian.py:333-336) is composed exclusively of module constants;run_stability_jobpasses onlyxyzandlevel. - No path traversal — the only path written to
output.ymlis ARC-constructed, never parsed out of a log. - Checkfile warm-start confirmed — each stability job is an SCF restart plus a matrix diagonalization, not a re-optimization. (Ironically, the same mechanism as finding 1.)
- No completed job is invalidated or re-queued on adoption.
- No per-tick re-parse —
check_stability_jobis called once, from the single job-completion branch at:869. - Parser cost at scale: a synthetic 210 MB log gives 1.73 s / 392 MB peak for
parse_wavefunction_stabilityand 1.51 s / 394 MB forparse_s_squared— the samereadlines()pattern every existing ARC parser uses, and real logs are orders smaller. - Parser false positives swept: across all tracked
.out/.logfixtures, only the four new stability fixtures parsed —parsed_count 4, errors_count 0. stabilitycannot block convergence and is correctly reset bydelete_all_species_jobs.
Mutants the suite killed — i.e. the tests work:
'unknown'→'stable'on an unreadable verdict: killed bytest_unparsed_verdict_is_not_reported_as_stable.number_of_radicals is None→ truthiness: killed, confirming declared-0is distinguished from undeclared-None.number_of_radicals > 1→>= 1: killed, confirming a declared1isn't credited as an open-shell source.- Q-Chem
len(tokens) >= 2→> 2: killed bytest_parse_s_squared_qchem.
Where this leaves the PR
Two chains account for nearly everything above.
Finding 1 is the review. guess=read from a restricted checkpoint returns E(RKS) exactly, so the adoption path changes no number today. Findings 3, 10 and 11 are downstream detail of the same defect: the feature measures correctly, records correctly, and then applies a reference flip that the SCF quietly undoes. Fix finding 1 and the PR does what its description says.
arc/checks/spin.py has no consumers, which is why findings 8 and 9 are latent rather than live. Every trap in it is harmless today and becomes a wrong published number the day someone wires it up. Fixing the API now costs a fraction of debugging it later. The same holds for finding 6 — invalidates_analytic_freq is computed and discarded.
A smaller third chain: findings 5, 6, 7 and 11 are all the same shape — a diagnostic that is computed, recorded, and never gated on. Acting narrowly by design is defensible, but four separate measurements currently reach no decision, and finding 7 is the one most likely to matter in a real campaign.
Method, for what it's worth
Eight parallel review passes over the same pinned diff — testing/mutation, security, API-contract, maintainability, performance, data-migration, an adversarial pass, and a cross-model adversarial pass — none of which saw each other's conclusions, plus the PySCF measurement above. Findings were deduplicated by file:line and by mechanism; cross-model findings were re-derived against the source before being accepted.
Three conclusions were reached independently by two passes each, which is why I'd weight them: adoption being inert for the measured species (finding 3), the provenance riding restart.yml rather than output.yml, and the TS-switch record losing its log path (finding 11). The restart spawn guard is recorded as unresolved precisely because two passes read the same lines and disagreed.
All test gates ran in arc_env with an empty HOME and pytest -n0. Known-failing at any SHA on my box, not attributable to this branch: checks/ts_test.py::test_check_rxn_e0, ::test_compute_rxn_e0, processor_test.py::test_compare_rates, statmech/arkane_test.py::test_run_statmech_using_molecular_properties (missing arkane in rmg_env), plus molecule_test.py::TestConnectTheDots.
No ESS job was submitted at any point.
f9c0a3a to
afc47f9
Compare
Thanks. Please note that the the branch has moved quite ahead since this review was enacted - I believe even when reviewed it had already moved a few commits ahead. So this will require another thorough review.
Fixed: 5, 6, 7, 8, 9, 10, 11, 12, H1, H2, H5, H6 Also, just for clarification:
|
afc47f9 to
3ea4292
Compare
|
Second review, at The round-one blocker is fixed, and I verified it by execution rather than by reading. A Both halves present: the The path the code now takes reaches the lower solution. Mutating Also fixed, each verified: the And the ZPE splice is genuinely closed.
That is a serious response to a hard finding. What follows is what the new surface brought with it. Blockers1. Molpro cannot honour an adopted verdict, and Gaussian-opt + Molpro-sp is the standard arrangement
The template hardcodes the reference block:
Scenario: singlet H-abstraction TS, opt at ωB97X-D/def2-TZVP in Gaussian, RHF→UHF instability adopted, re-opt in Gaussian reaches the broken-symmetry geometry, sp in Molpro. Molpro either errors or returns an RHF-based CCSD(T)-F12 energy at the broken-symmetry geometry — neither the restricted answer (wrong geometry) nor the unrestricted one (wrong reference) — while
The design question underneath it: is a log warning the right response when the ESS cannot honour the adoption? I'd argue no. The check is cheap and available before the sp is spawned. Refusing to adopt when the sp adapter is not in 2.
|
74529de to
6b414b2
Compare
2. BrokenSym 1,1 with * xyz 0 1 - So this one I disagree with. I ran the job with ** ORCA 6.0.0, !UKS B3LYP def2-TZVP TightSCF defgrid3, * xyz 0 1, %scf ... BrokenSym 1,1 end. ORCA builds the high-spin determinant internally from the Na,Nb operans - the log prints High-Spin SCF calculation and S(High-Spin) = 1.0 - then flips. <S2> runs 2.009324 → 0.864748 → 0.864748, energy -196.364789363862, matching to 1e-9 Eh both the !MORead-from-broken-symmetry-orbitals run and the solution ORCA reaches by following the instability. The coordinate line keeps the target multiplicity; the operancs supply the high-spin state. No change made. Also, emission is hard gated on multiplicity != 1, but charge is not - closed shell cation with an even electron count would get * xyz +1 1 with BrokenSym 1,1. Untested. **1. Molpro - True, it is a problem. So, Molpro is not the limitation. {uhf; wf, spin=0} is valid Molpro and ROTATE, i.sym, j.sym, 45 is how one requests a broken-symmetry singled. In this instance, ARC's adapter is the limitation. It hardcodes (molpro.py:50) {hf in the template, one occurrence, never varying — and byte-identical on main, so this PR didn't introduce it. Note also {hf} isn't RHF: arc/testing/trsh/molpro/insufficient_memory.out:199 shows {hf; wf,spin=1} → PROGRAM * RHF-SCF (OPEN SHELL), i.e. ROHF, and the u at :233 selects Molpro's UCCSD(T), which is the ROHF-orbital CC. A blanket So I measured the alternativ propelry. On the C5H10 singlet TS at cc-pVDZ {uhf; wf,spin=0} -194.974539 <S2> 0.000000 collapses to RHF and on 90°-twisted ethylene (D2d, deliberately symmetric) ROTATE with the naive n/2 index fails until symmetry,nosym is added, after which it works. So the recipe exists: nosym + {uhf} + rotate,{n//2}.1,{n//2+1}.1,45, scoped to adopted species, using count_electrons. Then, I ran it through the singlet point (proving why the gate stays - will come up with a future stack PR for it): reference SCF CCSD(T)-F12 T1 D1 The broken-symmetry reference is 64 kcal/mol lower at SCF and 140 kcal/mol higher after coupled cluster, recovering 0.35 Eh less correlation. You cannot substitute one for the other in a barrier. And the second row is the one that decided it: T1 drops from 0.041 to 0.0124 - under ARC's 0.015 warning threshold. The orbitals absorb the static correlation, so the diagnostic that exists to detect a multireference species stops detecting it. ARC already flags this TS correctly today; adopting would have replaced a flagged wrong number with a quietly-unflagged one. So, the gate is Scheduler.stability_verdict_can_be_honoured, evaluated when the verdict is recorded (before adoption takes effect), testing only the three levels the E0 is built from and skipping reference-agnostic ones. The verdict is still measured and recorded for every species - only the adoption is refused, and only for a verdict ARC would act on. All-Gaussian and all-ORCA are provably unchanged. True regarding STABILITY_CAPABLE_ESS/SYMMETRY_BREAKING_CAPABLE_ESS said "ESS that cannot" when they meant "adapter ARC has not taught". They're now STABILITY_ANALYSIS_ADAPTERS/SYMMETRY_BREAKING_ADAPTERS, documented at the definition, and the warning now names the physics: an open-shell singlet is a two-determinant state, a broken-symmetry reference approximates rather than describes it, and a multireference treatment (CASSCF → MRCI/CASPT2) is what such a species calls for. 3. Regarding ending unstable - yes, agreed and fixed. The sector is read off any solition the log relacxed into, not only a stable one; ORCA re-converges before each analysis, so the <S2> it stoppped on belongs to a converged determinant whichever try that was. A single-analysis log still measures nothing and stays unattributed_inability. THe item-12 followed_to_unstable contract defect is fixed in the same expression - it now requires the first block to have been unstable, so concatenated stable analyses aren't read as a follow. Declined the Restart requested: anchor as version-fragile. **4. stability_analysis_ran - agreed in part. Cleared when the verdict is dropped, kept when it's carried. Your second failure mode is real and the reset closes it. The first one though, `guess=mix and BrokenSym are initial guesses so on a genuinely closed shell saffle, the unrestricted SCF relaces back and E(UKS) = E(RKS). Measuring a carried verdict would also let a stable verdict on the unrestricted reference replace the adopted one mid-species, flipping the reference under queued jobs. **5. Zero byte .gbw - agreed and fixed. The guard went into JobAdapter.readable_checkfile, which now refuses a path naming no file and a file of size zero - covering _initialize_adapter, both adapters' species-checkfile fallbacks, Psi4 and TeraChem in one place. Both adapters job-directory fallbacks were bypassing it entirely and now go through it. **6. The pipe - traced and it does not bypass the reference deciison. _pending_pipe_sp/_freq/_irc are populated only inside spawn_post_opt_jobs, after the stability gate returns, so no piped job is composed before the verdict is in. build_species_leaf_task puts species.as_dict() in the payload, as_dict() emits derived_stability_verdict, and pipe_worker._run_adapter rebuilds the species and calls job_factory. Measured end to end on a real payload: the verdict survives the round trip, restricted_used=False, and both guess=mix and BrokenSym 1,1 are present. What the pipe genuinely skips is provenance - warn_on_collapsible_unrestricted_reference is called only from run_job, and _ingest_species_sp writes e_elect without post_sp_actions, so no record_scf_reference and no spin-contamination check. Both documented in advanced.rst 7-12 (excl. 11). Sentinel made sticky via **11. Reference flip scope - agreed/fixed. You were right about the 1.99kcal/mol and that CCSD(T) is not variational. With Molpro 2026, it showed it was larger than 1.99 for the C5H10 singlet TS: As we see, the broken symmetry reference is 64kcal/mol lower at SCF and 140kcal/mol higher after coupled cluster, recovering 0.35Eh less correlation. The fourth column shows that T1 falls from 0.0410 to 0.0124, below ARC's own 0.015 threshold. Adoption is now gated positively on the level opt and freq still adopt, so the re-optimisation onto the broken-symmetry geometry and the ZPE taken from it are unchanged. The single point stays restricted and emits no One consequence to be aware of: the documented default - Gaussian DFT opt/freq with a Molpro Also, another thign was found and fixed. stable=(rext,noopt)` can report RHF -> CRHF, and adopted_reference_is_unrestricted had no relaxation-class gate - such a verdict would have run as a real unrestricted determinant chasing a complex solution. Adoption now requires derived_instability_breaks_spin_symmetry(species) is not False. |
6b414b2 to
f95cd83
Compare
Round 3 — scoped to the broken-symmetry adoption gateReviewed only what changed since VerdictKeep the gate. Scoping adoption to levels whose energy is their determinant's energy is Three things need to change before it merges, and the first two are both about the 1.
|
| reference | SCF | CCSD(T)-F12 | E_corr |
|---|---|---|---|
| RHF | −194.974539 | −196.064366 | −1.089827 |
| BS-UHF | −195.077240 | −195.840652 | −0.763412 |
| ROHF triplet | −195.066148 | −196.053302 | −0.987154 |
The BS-UHF reference absorbs 0.1027 Eh (64.4 kcal/mol) of static correlation at the SCF
level, yet the correlated treatment loses 0.3264 Eh (204.8 kcal/mol). 0.2237 Eh goes
missing. CCSD(T) is not variational, but a 140 kcal/mol spread between two single-reference
CC treatments of the same singlet state at the same geometry and basis means at least one of
them is not a CCSD(T)-F12 energy of that state — not when T1(RHF) = 0.041 puts the
multireference character in "watch it" territory, not "the method has failed by 0.2 Eh"
territory.
The sharpest form: BS-UHF recovers 0.22 Eh less correlation than the ROHF triplet row,
which is also an open-shell F12 treatment of the same molecule in the same basis. Same code
family, same electron count. A spin-relaxed reference legitimately recovers a bit less; it
does not lose a fifth of a Hartree.
The likely mechanism is ARC-relevant, so worth naming: Molpro's ccsd(t)-f12 is the
closed-shell F12 code and uccsd(t)-f12 is the ROHF-orbital open-shell one. Neither takes a
genuinely spin-broken UHF determinant as its reference. {uhf; rotate,...} orbitals fed to
either gives a CC expansion about something that is not the BS determinant.
The gate does not need this number. The T1/D1 row does all the work and is independently
credible: 0.0410 → 0.0124 and D1 0.2172 → 0.0594 is exactly the documented behaviour of a
reference that has absorbed static correlation into its orbitals, and the diagnostic that
exists to catch a multireference species stops catching it. Note that the ROHF triplet row
demonstrates your point more cleanly than the BS row does — T1 = 0.0118 with a perfectly
ordinary total energy, i.e. spin relaxation suppresses T1 without any of the 140 kcal/mol
weirdness. Cite that, drop or heavily qualify the 140 kcal/mol and 0.35 Eh claims, and the
argument gets stronger, not weaker.
3. Double hybrids admit a broken-symmetry reference, and by your own argument they must not
advanced.rst:71-76 states the principle:
The energy of those levels IS the energy of their SCF determinant... A correlated
wavefunction level keeps its restricted reference, because its energy is a correlation
expansion built about a spin-adapted reference rather than the energy of that reference.
Executed against the shipped gate:
b2plyp type=dft admits=True
b2plypd3 type=dft admits=True
dsd-pbep86 type=dft admits=True
wb97x-2 type=dft admits=True
hf type=wavefunction admits=True
hf-3c type=wavefunction admits=False
ccsd(t)-f12 type=wavefunction admits=False
A double hybrid's energy is not its determinant's energy — it carries an MP2 term expanded
about the KS determinant. That is the same construction the docstring excludes, and your own
objection applies with full force: BS-MP2 about a spin-broken KS reference is the textbook
pathology. BROKEN_SYMMETRY_METHOD_TYPES = ['dft'] lets B2PLYP, DSD-PBEP86 and ωB97X-2 do
precisely what CCSD(T)-F12 is forbidden to do.
Not hypothetical for ARC: double hybrids are a normal sp_level choice, and for a
double-hybrid sp the mismatch check would also report reference_mismatch: false — one
reference throughout — while the number is built the way this gate exists to prevent.
Suggest excluding double hybrids explicitly (a DOUBLE_HYBRID_METHODS deny-list checked
before the method_type test is the smallest fix). hf-3c landing on admits=False is the
mirror image and much less consequential — worth a line either way so the two lists are
symmetric.
4. Confirmed as you described it
deduce_method_type really does put hf in 'wavefunction' — arc/level.py:344-371,
'hf' matches by substring — so Level(method='hf').method_type == 'wavefunction' and
admits=True only because of the BROKEN_SYMMETRY_METHODS fallback. Your reason for gating
positively rather than adding 'wavefunction' to REFERENCE_AGNOSTIC_METHOD_TYPES holds on
both counts: it would silence HF, and a reference-agnostic level makes
job_scf_reference_is_restricted return None, so record_scf_reference would never record
the sp and the mismatch detection would go dark entirely. Verified by reading both paths.
5. Restart and TS switch — checked, and clean
Both survive:
scf_referencesround-trips through the restart file (species.py:829-830writes it when
non-empty,:972restores it) andjob.restricted_usedis restored at
scheduler.py:5067-5070. A species that adopts for opt/freq, gets restarted, and then runs
its sp still reports the mismatch.carry_stability_verdict_across_ts_switchresetsspecies.scf_references = dict()
(:3879) and stripsMIXED_SCF_REFERENCE_MESSAGE(:3881-3884), so afreqreference
from an abandoned guess can never be compared against a new guess'ssp. And a verdict
carryingreference_change_available: Falsecan't leak into the rebuilt dict at
:3901-3905, becauseadopted_reference_is_unrestrictedreturnsFalsefor it and the
drop branch fires first. That's tighter than it needed to be; good.
6. Smaller
UNREACHABLE_REFERENCE_MESSAGEis not stripped on a TS switch. The list at:3881is
[MIXED_SCF_REFERENCE_MESSAGE, INVALID_ANALYTIC_FREQ_MESSAGE, SPIN_CONTAMINATION_MESSAGE, COLLAPSED_REFERENCE_MESSAGE].UNREACHABLE_REFERENCE_MESSAGE, appended at:2252, isn't
in it, so it outlives the guess it described whileoutput[label]['wavefunction_stability']
is set toNoneon the same pass. The method's own docstring says "THE TWO RECORDS ARE
REDUCED TOGETHER".post_sp_actionsatscheduler.py:1626-1629still passes nojob. Round-2 item 12.
The blast radius is bounded — that branch only runs whensp_level == opt_level, where a
reference mismatch is impossible by construction — so the effect isreference_mismatch: nullinoutput.ymlfor a restarted single-level project rather than a wrong number.
Worth a comment saying so, since the guard reads like an oversight.'rhf'inBROKEN_SYMMETRY_METHODSis unreachable:Level(method='rhf', basis='cc-pvdz')
raisesIndexError: list index out of rangeatlevel.py:450because no adapter registers
rhf. Pre-existing (level.pyis untouched here), not yours to fix, but the entry is dead.
'uhf'/'rohf'are constructible but moot, since adoption only fires for a species whose
reference was restricted.
Ranked for merge: (1) fix the run-summary claim — code or docs, either is fine, but they
have to agree; (2) strike or qualify the 140 kcal/mol and 0.35 Eh claims and lean on T1/D1;
(3) exclude double hybrids. (6) is cleanup. The gate itself, and the decision to accept a
mixed-reference E0 over a correlated energy expanded about a symmetry-broken determinant, I'd
merge as designed.
f95cd83 to
20a0f2a
Compare
6a. UNREACHABLE_REFERENCE_MESSAGE added to the strip list. It's always safe to strip: that warning is raised only on a verdict stamped reference_change_available: False, which the drop branch already reads, so such a verdict never survives the switch anyway. Separately, your item 3 turned up a main bug worth its own fix. Checking why cam-b3lyp reported admits=False, it turns out deduce_method_type matches semiempirical_methods = ['am', 'pm', …] as substrings, so 'am' inside 'cam' fires. Every Coulomb-attenuated functional — cam-b3lyp, camh-b3lyp, cam-qtp00, lc-camb3lyp — types as semiempirical. That matters beyond this gate: is_species_restricted early-returns True for semiempirical before any multiplicity check, so a doublet radical at CAM-B3LYP currently runs restricted (b3lyp → restricted=False, cam-b3lyp → restricted=True, same species). level.py is untouched here so I haven't fixed it in this PR. |
…t of ESS logs Two ESS-log readers that nothing in ARC could previously perform, plus the real logs that pin them. Neither is wired to a caller here; later commits consume them. THE WAVEFUNCTION STABILITY VERDICT. Gaussian's documentation states that "analytic frequency calculations are only valid if the wavefunction has no internal instabilities". ARC runs a freq job on every TS, so that precondition was assumed and never tested, and for a restricted singlet TS it cannot even be inferred after the fact, because a restricted wavefunction prints no <S**2>. parse_wavefunction_stability reads a stable=(rext,noopt) log and returns the verdict, the label and eigenvalue of each negative stability-matrix eigenvalue, and whether the analytic frequencies are invalidated. Whether an instability bears on the frequencies depends on the reference: Gaussian's rule is that a restricted wavefunction need only be free of singlet, i.e. internal, instabilities, while for an unrestricted one any instability invalidates the analytic frequencies. The reference is read from the log's own SCF Done: E(RwB97XD) / E(UwB97XD) line rather than predicted from the species, so the verdict and its consequence are derived once, here, from what Gaussian actually did; the scheduler line and output.yml both read that single result instead of recomputing it. A log that ran an analysis but whose verdict could not be read is reported as 'unknown', never as 'stable', so a gap in the recognised phrasings cannot pass for a clean bill of health, and invalidates_analytic_freq is left undecided rather than guessed when the reference cannot be read. A log with no stability analysis at all returns None. A STABLE UNRESTRICTED GAUSSIAN VERDICT REPORTS external_instability AS None RATHER THAN False, WHICH CHANGES WHAT THE GAUSSIAN READER RETURNS for that case. All four Gaussian fixtures, both UB3LYP ones included, hold exactly one <AA,BB:AA,BB> singles matrix and no <AB,BA spin-flip block, so for an unrestricted reference Gaussian never computes a spin-flip root and the external sector is not tested at all. Reporting False there asserts a test that was not run, and it disagreed with the ORCA reader, which reports None for the identical physical situation. A restricted reference is unaffected: its single matrix spans both sectors, so a stable verdict there still reports both flags False. invalidates_analytic_freq is unchanged in every case, and the cross-ESS test now asserts both flags rather than the verdict and reference alone. The two log-derived digit runs the readers feed to int() are guarded with arc.common.is_str_int, as the Q-Chem reader already guards its own: the regexes rule out a non-numeric match, but CPython's int_max_str_digits makes int() on a run of more than 4300 digits a ValueError, and a truncated or corrupt log can hold one. The six Gaussian fixtures' Link 0 line carried the submitting account, the compute node and the scratch layout of the machine they were run on. All three are replaced with a neutral scratch path; every parser this branch adds returns byte-identical results before and after. Four real Gaussian stable=(rext,noopt) logs of campaign TS geometries are added under arc/testing/stability/: a restricted singlet with an RHF -> UHF instability, a stable restricted singlet, a stable unrestricted doublet, and a stable spin-contaminated doublet. They establish two things the parser had assumed otherwise. A RExt run emits one analysis with one verdict line, not an internal and an external one. And the eigenvector symmetry label follows the reference: restricted logs label roots by spin (Triplet-A), unrestricted logs label them by the root's own spin expectation value (2.012-A). Reading the reference off the SCF Done line is confirmed on all four. THE S**2 SPIN-CONTAMINATION DIAGNOSTIC, RE-HOMED FROM AN ARCBENCH-BASED BRANCH. parse_s_squared, s_squared_expected_from_multiplicity, the ESSAdapter default and the Gaussian / ORCA / Q-Chem implementations come from feature_s_squared_spin_ diagnostic, with the Gaussian anchoring fix of fix_s_squared_stability_eigenvector folded in rather than applied afterwards, so the pre-fix parser is never present in this history. Left behind deliberately: that branch's arc/tckdb/ payload builder and its attachment to the sp calc, because arc/tckdb/ does not exist on main and bringing the builder would have meant inventing the module around it. The output.yml key it reads, sp_spin_diagnostic, is brought in the same shape and key names, so the arcbench-side emitter binds to it unchanged. WHY parse_s_squared SURVIVED OVER parse_spin_squared. A second, independently written Gaussian reader returning a bare float existed on a third branch. The two were compared line by line before discarding one. They agree on everything that could have made this a semantic merge rather than a deletion: both anchor on a line carrying <Sx>= and exclude the Initial guess line, so neither reads a Stable job's Eigenvector root spin as the reference's; both take the last such line, so both report the converged SCF rather than an earlier cycle; both take the value BEFORE annihilation of the first spin contaminant; and both return None for a restricted reference, which prints no spin line at all. Two differences were adjudicated: Return shape. The dict is kept. A bare float discards the annihilated value and the ideal S(S+1), both of which output.yml records, and cannot express "read the reference, but the log states no multiplicity". Numeric spelling. Fixed-point is kept over accepting a Fortran D exponent: Gaussian's spin line and its "S**2 before annihilation" line are both fixed-format F fields, and the D tolerance was speculative rather than fixture-driven. Keeping the deployed regex also keeps this file from diverging from the copy running the benchmark. The choice is stated in parse_s_squared's docstring so it is not silently re-litigated. Gaussian multiplicity anchoring takes the FIRST 'Charge = C Multiplicity = M' line: it is the symbolic Z-matrix echo of the job's own molecule specification, while any later one declares the multiplicity of a single fragment of a guess=fragment calculation, which is not the wavefunction's. arc/testing/spin/uhf_fragment_guess_doublet.out pins it. The initial-guess exclusion is pinned by a fixture that can only be read correctly with the guard in place: arc/testing/spin/uhf_died_before_scf_septet.out is a real septet that printed 'Initial guess ... <S**2>=12.0000' and then died in l502 on an inaccurate quadrature before any SCF Done. Without the guard the parser reports 12.0000, exactly S(S+1) for a septet, as a converged diagnostic of a wavefunction that never existed. With it the file yields None. Extracted, not fabricated. ORCA needs no equivalent anchoring: across all six ORCA fixtures the string 'Expectation value of <S**2>' occurs only inside the UHF SPIN CONTAMINATION block following a converged SCF, and MDCI's '<S**2>(linearized)' does not contain it. 'Last wins' is correct there and is now pinned. What was fixed is that 'Expectation value' and 'Ideal value' were latched independently, so a block missing its Ideal line would have inherited an earlier block's; the expected value is now reset when a new expectation value is read, tying the pair to one block. The vestigial `and 'Mult' in line` is removed: 'Mult' is a substring of 'Multiplicity', so the condition was unconditionally true. Cross-ESS multiplicity rules are assessed and left alone. Gaussian and Q-Chem take the first declaration, ORCA the last; each is right for its own format's dominant multi-declaration case, and all three are fallbacks only. The restricted contract is assessed and left alone. An RHF/RKS determinant is an exact eigenfunction of S**2 with <S**2> = S(S+1) exactly, and returning None for it conflates that with a parse failure. Returning the exact value would need a new reference-detection sniffer in each of the three adapters purely to undo what the parser just did, and would split the base class's contract under which None means 'no diagnostic available' for Molpro and CFOUR alike. ESSAdapter.parse_s_squared's docstring now states the conflation so a consumer knows to compute the restricted value from the species' multiplicity instead of reading a log. Deferred cleanup: s_squared_expected_from_multiplicity is homed in arc/parser/parser.py but does no parsing; it is pure spin arithmetic and belongs in arc/checks/spin.py. It duplicates nothing (checked). Moving it would touch three more adapters, so it is recorded here rather than done. Verified by mutation, not only by a green run. Making the Gaussian S**2 reader return None unconditionally fails 13 tests; restoring the pre-fix anchoring, which reads the last <S**2>= line whatever it sits on, fails 5, including the restricted Stable log that then reports a fabricated diagnostic where its contract is None; making the ORCA and Q-Chem readers return None fails 4 and 1; taking the last Gaussian multiplicity fails 1; dropping the initial-guess guard fails 1; deleting the ESS s_squared_expected fallback fails 1. ORCA reads the same verdict out of its own logs. OrcaParser gains parse_wavefunction_stability, returning the schema GaussianParser returns plus two keys for behaviour Gaussian has no equivalent of, and ESSAdapter gains a base declaration returning None so the parser can be dispatched to any ESS, as parse_s_squared already could. THE FIRST ANALYSIS IS THE ONE UNDER TEST. ORCA 6.0.0 aborts in LEANSCF on an unstable wavefunction unless it is told to follow the instability, so ARC always sets STABRestartUHFifUnstable true and an unstable log therefore holds TWO analyses with opposite verdicts: the wavefunction the frequency job built its Hessian from, then the solution ORCA relaxed into. The verdict, the lowest eigenvalue and the negative roots are read from the first; n_analyses and followed_to_stable report the second without overwriting it. The reference is read from the HFTyp line preceding the first analysis, so a restart to an unrestricted solution does not rewrite the reference tested. BOTH CODES TEST THE SAME SPACE, and the flags follow from that. ORCA analyses RHF/RKS in UHF/UKS space and UHF/UKS in UHF/UKS space, both Ms-conserving; all four Gaussian fixtures print 'Stability analysis using <AA,BB:AA,BB> singles matrix', which is the same Ms-conserving block, and Gaussian's Ms-changing <AB,BA:AB,BA> block appears in none of them. Neither code reaches the GHF sector, so neither verdict is the weaker one. The verdicts agreed on all four measured systems, and at matched functional (ORCA's B3LYP/G is Gaussian's VWN3; plain ORCA B3LYP uses VWN-5) the lowest roots agree to under 0.4% on the three systems where both codes found the same SCF solution. The fourth, a near-dissociated O(3P)...CH3 pair at r(O-C) = 3.78 A, has the two codes converged to DIFFERENT UHF solutions (total energies 0.025 Eh apart, <S**2> 1.7488 against 1.700055), so its roots compare two wavefunctions rather than two codes and support no cross-code conclusion. An earlier revision of this branch claimed the opposite and is corrected here, in the parser docstring and in the documentation. For an unrestricted reference an instability is spin-conserving, i.e. Gaussian's internal sector, and external_instability stays None since no spin-flip root is computed. For a restricted reference ORCA prints one unlabelled matrix spanning both sectors, so the sector is MEASURED rather than assumed: ARC is forced to run STABRestartUHFifUnstable true, so the log already carries the spin expectation value of the solution ORCA relaxed into. A nominal singlet reaching a stable solution at <S**2> above a small threshold broke the spin symmetry, which is external; one reaching a stable solution still at <S**2> of zero moved within the spin-conserving sector, which is internal. A restart that never reached a stable solution measures nothing, and the verdict is then 'unattributed_instability' with both flags None -- never 'stable', and never grounds for changing a reference. Assuming external, as the first revision did, made derived_reference_is_unrestricted fire on an unmeasured guess AND suppressed the analytic-frequency-invalidity warning that a genuine internal instability must raise. s_squared_after_follow reports the measurement, and the restart fixture's test asserts it. invalidates_analytic_freq now applies the same rule the Gaussian reader applies, so one physical situation gets one answer whichever ESS measured it. Two smaller reader fixes. An RO reference is reported as restricted None rather than as restricted True, and gets no 'RHF -> UHF' relaxation: run_stability_job admits 'rohf', and neither flag names a constraint an ROHF instability relaxes. And a root printed as -0.00000000 parses to negative zero, for which '< 0' is False, so an unstable verdict came back with an empty root list and a blank log detail; negative zero now counts as negative. The five fixtures are real ORCA 6.0.0 B3LYP/def2-TZVP logs of the same four geometries the Gaussian fixtures were taken on, plus the log of a job run with the restart key false, which crashes after printing its verdict and is kept to pin that the verdict is still readable out of an errored log. The test that asserted orca['lowest_eigenvalue'] > 100 * gaussian['lowest_eigenvalue'] is gone. It froze an artifact of two codes converging to different UHF solutions under a name asserting a mechanism that does not exist, and would have passed forever. What replaced it asserts what the fixtures support: the two codes agree on every verdict and on every reference, and they agree on invalidates_analytic_freq. The errored-log test keeps its assertions and loses its claim: the parser does read a crashed log, but check_stability_job returns early on a non-done status and a LEANSCF crash classifies as errored/['Unknown'], so ARC never surfaces that verdict -- it is parser robustness, not a product guarantee. THE ORCA SECTOR IS MEASURED OFF ANY SOLUTION THE LOG RELAXED INTO. ORCA re-converges the SCF before each analysis it runs and allows five follow attempts, so the <S**2> of the solution it stopped on is that of a converged determinant whether or not a further root remains, and a solution carrying a few tenths of <S**2> broke the spin symmetry either way. s_squared_after_follow is therefore read whenever the log opened on an unstable analysis and holds more than one, rather than only where the last one ended stable, and a restricted instability ORCA followed to a still-unstable solution is recorded as the external instability its <S**2> shows it to be. An instability ORCA never followed, a log holding a single analysis, has nothing to measure the sector from and stays unattributed. followed_to_stable additionally requires that the FIRST analysis was unstable, so a concatenation of stable analyses is not read as a followed instability and reports no <S**2> of a wavefunction the log never relaxed into. The eigenvalue line is matched with an unambiguous number pattern, so a line the analysis never writes is rejected in time linear in its length: the digits before an optional decimal point and those after it no longer describe the same characters, which is what made a long malformed line cost time quadratic in its length to reject.
E_LS = E_BS + [(<S**2>_BS - <S**2>_LS) / (<S**2>_HS - <S**2>_BS)] * (E_BS - E_HS) K. Yamaguchi, F. Jensen, A. Dorigo, K. N. Houk, Chem. Phys. Lett. 1988, 149, 537; applied to broken-symmetry DFT by T. Soda et al., Chem. Phys. Lett. 2000, 319, 223. Nothing reads it. No job is spawned, no verdict is consumed, no dispatch path is touched, and what Arkane receives is unchanged. The design it was first written for -- gating on a stability verdict, recomputing the saddle with a broken-symmetry reference and recording a projected energy -- was withdrawn by the chemistry review of its motivating dataset and is not revived. The arithmetic is here because it is self-contained and exactly derived, and because it is what says what an adopted unrestricted energy still is not: a broken-symmetry determinant is not a spin eigenfunction, it mixes in the higher multiplicity, so its energy lies ABOVE the spin-pure low-spin energy, while the restricted energy it replaces lies above the broken-symmetry one in turn. The ordering is E_projected < E_BS < E_restricted, so an adoption is a step toward the spin-pure energy that stops short of it, and ARC projects nothing. The module is registered in arc/checks/__init__.py, which listed common, nmd and ts and omitted spin, so arc.checks.spin raised AttributeError under the package's own access pattern. The source it was carried from was labelled WIP, and the label described the module as well as the withheld design. Four defects a quantum-chemistry review and an adversarial code review found independently are repaired here rather than carried on faith. GENERALISED BEYOND A SINGLET TARGET. The closed form that was implemented, (<S**2>_HS * E_BS - <S**2>_BS * E_HS) / (<S**2>_HS - <S**2>_BS), is the general expression with <S**2>_LS = 0 substituted in: it is singlet-only, and nothing in its name, signature, docstring or tests said so. A doublet TS, which is routine in ARC, was silently projected as if its spin-pure <S**2> were 0 rather than 0.75. For a broken-symmetry doublet / high-spin quartet pair with <S**2>_BS = 1.0, <S**2>_HS = 3.80 and a 0.1 Hartree gap the error is 0.0268 Hartree, 16.8 kcal/mol. The general form is implemented instead, and the target state is a REQUIRED argument with no default: both entry points take the multiplicity of the state being projected onto and derive <S**2>_LS from it through parser.s_squared_expected_from_multiplicity, which is the arithmetic ARC already had for exactly this value. A default of 0.0 would be the same singlet-only assumption made once more, silently, at every call site that omitted it, and the size of that mistake is the 16.8 kcal/mol above. A multiplicity that names no spin state -- missing, non-numeric, below one, non-finite -- is refused with a warning rather than treated as any particular one. BROKEN_SYMMETRY_S2_THRESHOLD had the same defect and is fixed the same way. It compared the ABSOLUTE <S**2>_BS against 1e-2, not its deviation from S(S+1), so for any non-singlet broken-symmetry reference the flag was unconditionally True and therefore carried no information -- a clean doublet at <S**2> = 0.7536 read as 'symmetry broken'. The comparison is now against s2_bs - s2_ls, which the signature can express only because s2_ls is now an argument. THE SEPARATION GUARD IS A PHYSICAL FLOOR, NOT A DIVISION-BY-ZERO GUARD. MIN_S2_SEPARATION was 1e-3, which prevents a ZeroDivisionError and nothing else. Two ordinary UHF doublets at <S**2> 0.7540 and 0.7560, a pair that should never have been projected at all, returned -209.803 Hartree from energies of about -195.29, i.e. 14.5 Hartree and some 9,100 kcal/mol below its own E_BS, reported as a number rather than as None. A genuine broken-symmetry singlet / high-spin triplet pair is separated by about 1.0 and a doublet / quartet pair by about 3.0, so the floor is raised to 0.1: below that the two references do not describe two distinguishable spin states. At a floor of 0.1 no division-by-zero guard is needed on top. THE AMPLIFICATION IS BOUNDED AS WELL, so the floor is not a cliff. A separation floor alone says nothing about the result: at a separation a hair above it the ratio multiplying (E_BS - E_HS) is unbounded, so the first pair admitted past the guard can return an arbitrarily large correction, while the pair a hair below it is refused on logger.debug, i.e. silently. Both are addressed. Every refusal is a logger.warning, because a numeric routine that declines to answer has to say so. And the quantity that decides how far the projection moves the energy, (<S**2>_BS - <S**2>_LS) / separation, is capped directly at MAX_PROJECTION_AMPLIFICATION = 2.0. That ratio is w / (1 - w) in the high-spin weight w of the BS determinant: an ideal, fully spin-flipped broken-symmetry solution has w = 0.5 and a ratio of exactly 1, so a ratio above 1 means the BS determinant carries more high-spin than target-spin character. The cap admits w up to two thirds and refuses beyond it, where the correction exceeds twice the BS-to-HS gap. The largest correction any accepted projection can apply is therefore 2.0 * |E_BS - E_HS|, and the first pair accepted past the separation floor is bounded by the same amount as every other. THE INVERTED CASE IS A DIFFERENT FAILURE and no longer shares a branch with the near-degenerate one. For a variationally converged UHF/UKS determinant <S**2> >= S_z(S_z + 1) always -- spin contamination only ever adds -- so for a properly matched pair at the same geometry and level <S**2>_HS > <S**2>_BS is guaranteed, not merely typical. An inversion therefore does not mean 'too close to project'; it means the two references are not the same calculation: the HS SCF converged to a different state, the geometries or levels differ, the arguments were transposed, or an SCF did not converge. It has its own branch and a logger.warning naming the inversion, worded distinctly from the benign near-degenerate one, instead of the arithmetically true but diagnostically misleading 'separated by -1.0, below the 0.001 required'. The mirror-image inconsistency, a broken-symmetry <S**2> below the target state's own S(S+1), is refused and warned about on the same grounds. NaN AND INFINITY BYPASSED EVERY GUARD, since NaN < 0.1 is False. s2_bs = NaN returned NaN, and get_spin_projection reported broken_symmetry = False for it, which reads as an affirmative 'this reference did not break symmetry' when the truth is that <S**2> is unknown. All five inputs are now validated with math.isfinite, and the three <S**2> arguments additionally for non-negativity, which no expectation value of S**2 can violate. broken_symmetry is None, never False, whenever it cannot be judged, as the docstring already promised for the projected energy. The docstring said 'interpolating in <S**2> between the BS reference and the high-spin reference'. It is extrapolation: the low-spin target lies outside the interval the two references bracket, always, which is the whole point of the scheme. The stated reason for refusing an inverted pair, that it 'places the low-spin state outside the interval the two references bracket', was wrong for the same reason. Both are corrected. The tests are rewritten against literal numbers. The originals referenced MIN_S2_SEPARATION and BROKEN_SYMMETRY_S2_THRESHOLD symbolically -- for instance s2_hs = 2.0 + MIN_S2_SEPARATION / 2 -- which is tautological: it holds for any value of the constant, so mutating 1e-3 to 1e-12 and 1e-2 to 1e3 left all 12 tests green. The constants are now exercised through literal values that fail if any moves, and all three are additionally asserted directly so that changing one is a deliberate act. Each comparison boundary is pinned at the boundary itself: a separation of exactly MIN_S2_SEPARATION is projected and anything below it is not, an amplification of exactly MAX_PROJECTION_AMPLIFICATION is projected and anything above it is not, and a deviation of exactly BROKEN_SYMMETRY_S2_THRESHOLD is not reported as symmetry broken. 36 tests, from 12. Verified by mutation, not only by a green run. Returning None unconditionally fails 13 tests; inverting the sign of the projection term fails 8; replacing the separation guard with a never-taken branch fails 3; reverting the non-finite validation to the original `any(value is None ...)` fails 3; MIN_S2_SEPARATION 0.1 -> 1e-12 fails 4; and BROKEN_SYMMETRY_S2_THRESHOLD 1e-2 -> 1e3 fails 3. Each of the last two survived every one of the original 12 tests. The three boundary comparisons were mutated one at a time: the separation guard's < to <= fails 2, the symmetry-breaking > to >= fails 1, and the amplification cap's > to >= fails 1. THE RECORD NAMES WHAT PRODUCED THE ENERGIES. get_spin_projection takes the level of theory and the geometry as required arguments and carries both, along with the target multiplicity and the <S**2>_LS derived from it. The scheme extrapolates between two points of ONE potential energy surface, so a pair taken at two levels, or each from its own state's optimized geometry -- which ARC has lying around for the high-spin state -- is not a pair the projection is defined for. Nothing in the record said which surface its two energies came from; now it does. A BROKEN-SYMMETRY <S**2> BELOW THE SPIN-PURE TARGET BY LESS THAN MIN_S2_SEPARATION is the noise of a determinant that is spin-pure to within that separation, so its amplification is taken as zero and the projected energy is E_BS itself. Amplifying by a negative number returns an energy above E_BS, since E_BS - E_HS is negative, which is the wrong side of the reference the projection starts from and contradicts E_projected < E_BS. A multiplicity is 2S + 1 for a total spin S that is whole or half-integral, so it is a positive integer; a fractional value names no spin state and is refused with the warning every other unusable multiplicity gets, rather than read as the quarter-integral spin the arithmetic would otherwise give it.
Registers a new job type, off by default, that submits one Gaussian stability analysis at the freq level of theory and on the freq geometry. PLACEMENT. Stable is itself a Gaussian job type keyword, and Gaussian documents that only one job type keyword should be specified, the exceptions being Opt Freq and Polar Freq. So the keyword cannot be appended to the TS freq route. It could syntactically be appended to ARC's sp route, which carries no job type keyword, but ARC's sp is typically a composite or wavefunction-method energy, where stability analysis is unavailable (it is documented for HF and DFT only) and where a second job type keyword would displace the energy of record. A separate job at the freq level tests the wavefunction the Hessian is built from, and follows the shape of ARC's existing 'orbitals' job: a diagnostic that nothing depends on. KEYWORD. The route emits stable=(rext,noopt). NoOpt is Gaussian's default and is stated explicitly: it reports an instability without reoptimizing the wavefunction into the lower solution, so no rotated orbitals are produced. ARC also never propagates this job's checkfile -- species.checkfile is only repointed from opt, optfreq and composite jobs, and from troubleshooting, which this job is exempt from. Stable=Opt, RepOpt and 1Opt are never emitted, and complex-orbital testing is not enabled. RExt is kept over Int: it costs nothing extra, and Int would discard the broken-symmetry information the diagnostic exists to count. The job is skipped unless the freq level is HF or DFT and a checkfile exists to start from; without one the route would fall back to guess=mix, whose deliberately symmetry-broken SCF is a different wavefunction than the one under test, and which would then report itself stable. THE DIAGNOSTIC DEFAULTS TO OFF, in both default_job_types and initialize_job_types. Enabling it by default would make every species wait on a job that only a Gaussian species can satisfy, and on a run restarted from a restart.yml written before this job type existed it would raise KeyError in check_all_done, which reads output['job_types'][job_type] before reaching the exemption and is not backfilled by initialize_output_dict. JobAdapter.as_dict also gains restricted_used, the SCF reference this job's input declared. It is the one job attribute a restart cannot rebuild: restore_running_jobs calls job_factory, which calls set_files, which composes the input file again and so calls is_restricted against the species' CURRENT state, so a restricted sp job queued before a reference decision changed would come back from a restart claiming to be unrestricted. Narrowing the docstring to admit the memo is only session-durable was rejected -- reading the memo instead of recomputing is the entire reason the per-job records are trustworthy, and a record that is right until the run is interrupted is not a record. ARC's end-of-run status report prints the stability summary string alongside a converged species, so the diagnostic is visible without opening output.yml. Dropping restricted_used from as_dict fails 1 test; the job-type registration is pinned by the job-type dictionaries in arc/main_test.py. The project forbids a file carrying both `import X` and `from X import Y` for the same X. arc/main_test.py, which this commit edits, carried `import unittest` alongside `from unittest import mock`. The submodule import is spelled `import unittest.mock` and its four call sites are qualified, so the file now imports `unittest` one way only. The base class also stops hard-coding the checkfile name. local_path_to_check_file was 'check.chk' for every adapter, which is a Gaussian name that psi_4 and terachem happen to share; ORCA writes its orbitals to a file named after the input file. JobAdapter now resolves two names per ESS: check_file_name, the file the ESS writes its orbitals to and the file that is downloaded, and guess_file_name, the name a previous job's orbitals are uploaded under. They differ only where an ESS cannot read and write one file the way Gaussian reuses a single checkfile. Both default to 'check.chk', so Gaussian, psi_4 and terachem are unchanged. check_file_name and guess_file_name are per-subclass class attributes on JobAdapter, overridden in OrcaAdapter, rather than a registry in the base class keyed by adapter name: the base class should not enumerate its subclasses, and the per-subclass attribute is the idiom job_adapter itself already uses. JobAdapter also gains readable_checkfile, which refuses a checkfile whose base name is neither the adapter's own check_file_name nor the '<prefix>_<check_file_name>' form ARC writes for a directed rotor. Scheduler hands every job the checkfile its species holds whichever ESS wrote it, so without this an ORCA input.gbw reaches Gaussian and is uploaded as check.chk and read with guess=read. The hazard predates this branch through terachem; this branch makes it live for the two ESSs people actually mix, so it is closed here. READABLE_CHECKFILE ALSO REFUSES A PATH THAT NAMES NO FILE AND ONE NAMING AN EMPTY FILE. SSHClient.download_file leaves a zero-byte file behind where the download failed, and an SCF handed one either errors or starts from the guess it would have started from anyway while the input claims to read orbitals it does not have. Its docstring says what the method tests, the ESS that wrote the file and whether it holds anything, and that it examines neither the directory the path points into nor its relation to the project directory. The Gaussian adapter's fallback to an orbitals file sitting in its own job directory is refused while the species carries an adopted wavefunction-stability verdict and holds no checkfile. The species is holding none deliberately in that state: the orbitals it dropped describe the restricted reference the verdict rejected, and an unrestricted SCF seeded from them returns to that solution. The job directory of a job whose name a previous job of the same species already carried holds exactly such a file, and the route to the lower solution there is guess=mix rather than guess=read. The fallback also goes through readable_checkfile, so an empty file left in the job directory is refused the same way one handed in from the species is. THE RUN SUMMARY PRINTS THE WARNINGS OF A CONVERGED SPECIES, not only of a failed one. A species that mixes SCF references converges, so the failure branch that printed output['warnings'] never reached it and MIXED_SCF_REFERENCE_MESSAGE, raised for exactly the configuration the reference gate exists for, reached neither the summary nor any other in-band trace: outside scheduler.py nothing downstream consumes reference_mismatch. The invalid-analytic-frequency and spin-contamination warnings are raised on a converged species too. The converged branch now prints them alongside the stability line, and summary()'s docstring says so.
Adds the ORCA side of the 'stability' job type: a single point at the frequency level and on the frequency geometry that adds STABPerform and STABRestartUHFifUnstable to the existing %scf block, reading the orbitals of the job under test. THE INSTABILITY IS ALWAYS FOLLOWED, and that is not a preference. With STABRestartUHFifUnstable false, ORCA 6.0.0 prints the verdict and the stability-matrix roots and then dies in LEANSCF with a BLAS incompatible-matrices error and mpirun exit code 62. Measured at eight processes and at one, and at six roots and at three, so it is not an MPI artifact; LeanSCF false does not help, failing earlier, in the SCF, before any verdict is printed. The three stable jobs run to a normal termination on the same settings, so it is the re-entry into LeanSCF after an instability that breaks. ORCA therefore has no equivalent of Gaussian's NoOpt, which reports an instability without following it. With the key true the job terminates normally and the log holds two analyses; the parser reads the verdict of the first, which is the wavefunction under test. A crashed job would additionally be misread by determine_ess_status, whose orca branch matches 'error termination in SCF' and not 'in LEANSCF', so the job would be an unrecognised error and the verdict never read. THE ORBITALS UNDER TEST ARE HANDED OVER, because ORCA's analysis is an SCF post-step: it converges an SCF first, and from its own initial guess that need not be the solution the frequency job reached. This is the hazard Gaussian's checkfile requirement exists to prevent, and ARC's scheduler already refuses to spawn the job unless the species still holds the checkfile its frequency job used. ORCA names its own orbitals after the input file, so it cannot read and write one file the way Gaussian reuses a single checkfile: the previous orbitals are uploaded as guess.gbw and read with !MORead and %moinp, while the job's own input.gbw is downloaded and becomes the next job's guess. The adapter adopts a checkfile on construction the way the Gaussian adapter does. INPUT.GBW IS DOWNLOADED ONLY WHERE SOMETHING READS IT. A def2-TZVP .gbw runs to tens of MB and the job type is off by default, so downloading one from every ORCA job would cost every run bandwidth and disk for a file nothing opens. It is fetched for the job types the guess chain actually reads from -- the opt, optfreq and composite jobs Scheduler.end_job adopts a checkfile from -- plus the stability job, whose own orbitals are the relaxed solution. A job array takes the data.hdf5 branch and fetches no orbitals at all, since its members share one remote path; the docstring now says so rather than leaving it to be inferred. EVERY ORCA JOB THAT RUNS AN SCF READS THE GUESS, as every Gaussian job carrying a checkfile gets guess=read. OrcaAdapter.reads_orbital_guess is the single predicate behind both halves of it, the !MORead and %moinp keywords and the guess.gbw upload, so the file is uploaded for exactly the jobs that read it; emitting the keywords without the file aborts the job on a missing guess. ORBITALS_GUESS_JOB_TYPES holds the job types this adapter writes an SCF on one starting structure for: opt, conf_opt, optfreq and scan, whose first SCF the guess seeds and whose later points ORCA propagates orbitals through itself, and freq, sp, conf_sp and stability, each a single SCF. The rest are the job types write_input_file emits no keyword for, so ORCA is handed no calculation for a guess to seed -- composite, for which ORCA offers no composite method; irc and orbitals; and directed_scan, for which this adapter writes neither the scan block nor the constraints such a job needs -- plus gen_confs, tsg and onedmin, which belong to other adapters. A job array is excluded because it writes no input file and its members share one remote path, where one uploaded guess would stand in for every member, and a monatomic species because ARC spawns it neither an optimization nor a frequency job. WHAT THE CHAIN BUYS IS MEASURED. On a C5H10 TS at UKS B3LYP/def2-TZVP, same geometry and same input but for the guess, a fresh guess collapsed to the closed-shell solution (E = -196.344572 Eh, <S**2> = 0.000000) while !MORead held the broken-symmetry solution (E = -196.364789 Eh, <S**2> = 0.864739), reproducing the followed solution to 1e-9 Eh -- 12.7 kcal/mol apart. Without the chain the freq job converges from ORCA's own initial guess while the stability job reads the optimization's orbitals, and those are two different SCF solutions in exactly the cases the analysis exists to find. NO LEVEL OR BASIS IS TRACKED, because ORCA projects a guess written in another basis onto the basis of the job reading it. A def2-SVP job reading a def2-TZVP .gbw logs 'Atom 0: N(Shells)= 6 and 11 - projection required' and terminates normally at a sane def2-SVP energy, so the chain crosses the basis change ARC makes between the optimization and the single point on its own. The predicate is whether a checkfile this ESS wrote exists, and no new state is stored on the species, in the restart dict or in output.yml. The single point runs on defgrid3, the grid a frequency job uses, rather than the defgrid2 an sp would take, so the SCF under test integrates on the grid the Hessian was built on. The input template gains two placeholders that render empty for every other job type, so every existing ORCA input is emitted byte for byte as before. The test module's project directory moves from a shared path under arc/testing to a private tempfile.mkdtemp(), which is what the project requires of writable test scratch. It adopts the exact form open PR #1008 uses for the same file -- cls.scratch_dir = tempfile.mkdtemp(prefix='arc_test_orca_') with project_directory=os.path.join(cls.scratch_dir, 'test_OrcaAdapter') -- so the two PRs' overlapping lines are textually identical and merge without conflict. AN EMPTY ORBITALS FILE IS NOT READ AS A GUESS. reads_orbital_guess tests the size of the checkfile as well as its presence: the server-side copy of a .gbw an ORCA job died before writing is silent, and a failed download leaves a zero-byte file behind, so the file can be present and empty at the moment the input is composed. The adapter's fallback to an orbitals file sitting in its own job directory goes through readable_checkfile, which applies the same test, and is refused outright while the species carries an adopted wavefunction-stability verdict and holds no checkfile, in which state the route to the lower solution is BrokenSym rather than a guess.
…d not
The stability job was added as a pure diagnostic, and the argument that nothing should
branch on its verdict was about TS SELECTION: switching guesses on an instability would
burn the guess list to no effect, since within one reaction six geometries shared an
eigenvalue to seven decimals, and it would bias rather than filter, since in one
reaction the three lowest saddles were the unstable ones. None of that is touched. No
guess is rejected, no job is re-run, and no check gates on the verdict. What changes is
the one thing the diagnostic is direct evidence for and nothing else in ARC measures:
whether the restricted reference is the ground state.
THE CONTRACT.
1. A user-declared number_of_radicals ALWAYS wins, and is never overwritten by a
calculation.
2. ARC still runs and still assesses the check when the user declared a value.
3. Disagreement is a WARNING, never a crash. Both pictures are recorded; the user's is
the one used.
4. With nothing declared and an external (R -> U) instability of a RESTRICTED reference
reported, ARC adopts the measured verdict for the reference decision on subsequent
jobs and records the provenance.
WHERE THE DERIVED VALUE LIVES. NOT in number_of_radicals. That field is read in 13
places, 8 of them molecular-graph perception and validation (six sites in species.py and
the scheduler's four n_radicals= call sites), plus xtb_adapter.py's
`uhf = number_of_radicals or multiplicity - 1`. A measured SCF property must not steer
graph perception or an xTB UHF count, so a declared radical count and a measured
wavefunction verdict cannot share a field. ARCSpecies gains
derived_stability_verdict instead, defaulting to None, serialised through as_dict /
from_dict only when set. It is deliberately not an __init__ keyword: it is not user input
and there is no way to declare it. The name avoids
output[label]['wavefunction_stability'], which is the summary STRING the run report reads
and is a different object. ARCSpecies also gains scf_references, the per-job-type record
of which reference each completed job actually declared.
is_species_restricted consults the verdict LAST. The multiplicity > 1 branch is untouched,
the signature and the species=None fallback are unchanged, and the new branch is reached
only when number_of_radicals is None. Only an EXTERNAL instability of a RESTRICTED
reference flips it. An INTERNAL instability must never flip the reference: it is a lower
solution inside the reference's own spin symmetry, which is a different problem and is not
evidence of broken-symmetry character. Nor does an external instability of an
already-unrestricted reference, which says nothing about a restricted one.
THE PRECEDENCE IS WRITTEN ONCE. adopted_reference_is_unrestricted is the predicate for
"a verdict ARC acts on", and it is the only place the rule that a declared
number_of_radicals of ANY value blocks adoption is stated; is_species_restricted,
open_shell_character_source and the scheduler's TS-switch carry all read it rather than
restating it. A declaration of 0 or 1 asks for a restricted reference, so it blocks the
verdict as surely as a declaration of 2 imposes an unrestricted one; a predicate that only
checked for "no declaration at all" would carry a verdict across a TS switch for a species
that will never run on it.
MEASUREMENT WIDENS TO EVERY RESTRICTED SPECIES. ADOPTION STAYS TS-ONLY. These are two
decisions, and left alone the first would have made the second for free.
Variationally E(UKS) <= E(RKS), with equality if and only if the restricted solution is
stable. So for a stable closed-shell species the two references give the same number, and
a stability analysis is the only thing that says whether holding a species restricted
changed its energy at all; an already-unrestricted job is free to break spin symmetry and
has nothing to learn from the test. That is why the measurement gate admits a RESTRICTED
reference rather than testing multiplicity: job_scf_reference_is_restricted reads the
optimization job's own restricted_used memo, which is what that input actually declared,
whereas recomputing answers what the species would get today. `is True` and not truthiness, because
the helper returns None for a job with no memo, so a pipe task is refused rather than
admitted by accident. Force field, composite and semiempirical levels are excluded:
is_species_restricted returns True for them before any other consideration and ARC writes
no r/u prefix, so their flag is not a reference choice ARC made. A multi-species memo is a
list, not a decision, and is refused. record_scf_reference had open-coded the same two
exclusions and now calls this helper instead of carrying a second copy.
Adoption is refused for a well, deliberately. Acting on a verdict means re-optimizing on
the lower solution and running every job after it there, and the energy that produces is a
broken-symmetry one: spin-contaminated and unprojected, so it still sits above the
spin-pure energy of the state it is reported for. The blast radius of
writing such a number differs in kind between the two cases. A TS's energy prices one
barrier; a well's prices its own thermo and every reaction it appears in, through Arkane
and through AEC/BAC corrections parameterised against the reference ARC normally picks. So
a well is not moved onto a contaminated surface on the strength of a measurement of its
reference alone, while a TS, which has no thermo of its own and whose remaining jobs'
reference is the decision the analysis informs, is. Declaring number_of_radicals = 2 runs
a well unrestricted from its first job, consistently -- the widened diagnostic is exactly
what tells a user to do that. So "derived" stays a property of the verdict and "adopted"
is the verdict ARC acts on, and adoption is a TS's undeclared verdict only.
WHAT A WELL'S VERDICT IS FOR, GIVEN THAT IT WILL SAY 'stable'. This is not a hunt for
instabilities. Well under a few per cent of closed-shell equilibrium geometries are
RHF -> UHF unstable; a stretched partial bond at a saddle is where the instability lives.
The diagnostic is worth its cost for what a 'stable' verdict LICENSES: a well verified
stable has identical restricted and unrestricted energies, so a barrier or reaction energy
taken between it and a TS that ARC has made unrestricted is a difference on one surface
rather than a comparison across two. Second, it catches an undeclared singlet biradical,
whose restricted energy is simply wrong and which nothing else in ARC detects. The
docstrings say this, so that a long run of 'stable' verdicts is read as the diagnostic
working rather than as it having nothing to do.
open_shell_character_source reports 'declared' only ABOVE one. Zero and one attribute no
open-shell character beyond the multiplicity -- is_species_restricted turns a declaration
into an unrestricted reference only at 2 -- so naming them as the source contradicts what
the function is for. They report None while declared_number_of_radicals still carries the
value, so output.yml says both that nothing was attributed and that a declaration was
nevertheless present, which is what blocked the measured verdict.
is_restricted memoizes its decision on the job adapter as obj.restricted_used. Adapters
call it while writing their input, so the memo is the reference that job's input actually
declared.
THE COMPOSITE EARLY RETURN IS ASSESSED, NOT CHANGED. is_species_restricted returns True for
force_field, composite and semiempirical levels before any multiplicity check, which makes
'uCBS-QB3' unreachable through this path. A derived instability on a composite-method
species is therefore ignored, and bypassing is deliberately not done here: it would change
composite energies repo-wide, since Gaussian's CBS-QB3 already selects UHF internally above
multiplicity 1 and forcing the prefix applies UHF to every step of a recipe whose
extrapolation and empirical corrections are parameterised against the standard one. It
would also be worst exactly here -- the stability job runs only at DFT or HF levels, so a
composite-level species can carry a verdict only when its freq level is DFT while its sp
level is composite, and bypassing would flip the reference of the run's most consequential
energy on a diagnostic measured at a different level of theory. Pinned by a test so a later
bypass is a deliberate act.
WHAT AN ADOPTED ENERGY IS NOT. A broken-symmetry energy is not a spin eigenfunction: it
mixes in the higher multiplicity, so it lies ABOVE the spin-pure low-spin energy, and the
restricted energy it replaces lies above the broken-symmetry one in turn. The ordering is
E_projected < E_BS < E_restricted, so an adoption is a step toward the spin-pure energy
that stops short of it rather than a step past it. ARC projects nothing, so the residual
error after an adoption is the contamination, not the reference, and it keeps the sign and
direction it had. That is said in adopted_reference_is_unrestricted rather than left for a
reader to infer from the absence of a claim.
Two test fixtures were also corrected, and neither is a behaviour change: the Gaussian
adapter helper built its species from a lone oxygen atom at multiplicity 1, i.e. singlet O,
when O(3P) is the ground state, and the species round trip hung an external instability on
ethane. Both are pure plumbing tests that never touch chemistry. They are water and ozone
now -- the second being the textbook closed-shell singlet with genuine diradical character,
which is the species the verdict under test would actually be measured on.
Verified by mutation. Deleting the derived branch from is_species_restricted fails 3;
ignoring the reference the verdict was measured on, 2; letting an internal instability flip
the reference, 3; dropping the declaration guard from adopted_reference_is_unrestricted, 1;
removing the is_restricted memo, 2, and renaming it, 2; dropping 'composite' from
REFERENCE_AGNOSTIC_METHOD_TYPES, 3; crediting any declaration as the source, 1, and
crediting a declared 1, 1; inverting the non-TS measurement admission, 3; deleting the
non-TS branch, 2; requiring a restricted reference of a TS as well, 4; admitting anything
not explicitly unrestricted, 2; reading a per-species list as one decision, 1; reading the
reference under a name other than the memo's, 7; widening adoption to wells, 5; and
dropping the is_ts term from adopted_reference_is_unrestricted, 1.
One mutant SURVIVES and is left unpinned: replacing
`job_scf_reference_is_restricted(job) is True` with plain truthiness. It is an equivalent
mutant -- the helper's isinstance guard means its codomain is exactly {True, False, None}
and the two spellings agree on every element of it. The `is True` is a guard against a
future widening of that return type, and is documented as such rather than tested.
REQUIRED AT MERGE WITH feature_gaussian_trsh_remedies, WHICH IS NOT EDITED HERE. That
branch gates guess=mix in arc/job/adapters/gaussian.py on
elif any(spc.multiplicity == 1 and spc.number_of_radicals is not None and spc.number_of_radicals > 1
for spc in self.species):
It needs the derived term, or the derived verdict will change the u prefix without changing
the guess keyword and the symmetry-broken SCF will have no broken guess to start from:
elif any(spc.multiplicity == 1
and ((spc.number_of_radicals is not None and spc.number_of_radicals > 1)
or adopted_reference_is_unrestricted(spc))
for spc in self.species):
adding adopted_reference_is_unrestricted to that file's existing
`from arc.job.adapters.common import (...)` block. No separate `number_of_radicals is None`
guard is needed on the derived term: the predicate carries it, which is the point of having
one predicate. It now also excludes a well, so a well cannot get a symmetry-broken guess for
a reference ARC did not change.
adopted_reference_is_unrestricted's docstring now says where the residual error lands, not
only that it exists. Adoption acts for a TS only, so a TS whose restricted reference was
unstable runs unrestricted while its reactants and products stay restricted; the
adopted TS energy still sits above the spin-pure one while the wells, whose restricted
references are stable, carry no such contamination, so the barrier is systematically
OVERestimated by the residual contamination of the TS -- less so than the all-restricted
barrier it replaces, which sat higher still. The direction is what a user meets, and it was
the one thing the paragraph did not state.
A VERDICT THE RUN'S ESSs CANNOT REACH IS REPORTED AND NEVER ACTED ON. A verdict carrying
REFERENCE_CHANGE_AVAILABLE_KEY set to False is not one adopted_reference_is_unrestricted
returns True for, so it decides no reference and is credited as no open-shell character
source, while derived_reference_is_unrestricted still reports the measurement it holds.
The key records whether every ESS the species' E0 is built from can be given a
symmetry-breaking reference, which the scheduler decides when the verdict is recorded.
species_may_read_previous_orbitals reports whether a job of a species may adopt an
orbitals file the species itself does not hold. A species carrying an adopted verdict and
no checkfile holds none deliberately, and the adapters read that answer before falling
back to whatever sits in their own job directory.
The derived_stability_verdict attribute is documented as one the run that measures it
writes and a restart file reads back, like every other attribute, rather than as one no
input can carry: number_of_radicals is the input that declares open-shell character.
A DOUBLE HYBRID ADMITS NO BROKEN-SYMMETRY REFERENCE, and DOUBLE_HYBRID_METHODS is read before
the method type because ARC types one as DFT. A double hybrid's energy is not its Kohn-Sham
determinant's: a perturbative second-order correlation term is added to it, expanded about that
determinant, which is the construction level_admits_a_broken_symmetry_reference already excludes
the correlated wavefunction methods for. BROKEN_SYMMETRY_METHOD_TYPES = ['dft'] alone let B2PLYP,
DSD-PBEP86 and wB97X-2 take a spin-broken reference that CCSD(T)-F12 is refused, and BS-MP2 about
a spin-broken KS reference is the pathology the gate exists to prevent. Double hybrids are a
normal sp_level choice in ARC, so the case is not hypothetical.
The list is a deny-list rather than a classification of every functional: ARC has no double-hybrid
predicate to reuse -- deduce_method_type in arc/level.py knows only composite, wavefunction,
semiempirical, force field and DFT, and data/ess_methods.yml marks the group with a YAML comment
whose grouping has since drifted, PW6B95 and MN15 sitting inside it. Names are matched with their
hyphens and underscores dropped so a level written either way is recognized, and a double hybrid
the list does not name is admitted as ordinary DFT.
HF-3c IS ADMITTED, the mirror image of the same question. Its geometrical counterpoise,
dispersion and short-range basis corrections are additive functions of the nuclear coordinates
rather than of the wavefunction, so the level's energy is still the energy of its determinant plus
a number the reference does not enter. It carries the 'wavefunction' method type and is not spelled
'hf', so the name list is what admits it.
'rhf' IS KEPT IN BROKEN_SYMMETRY_METHODS. Level(method='rhf', basis='cc-pvdz') raises IndexError
in deduce_software only because rhf is registered for TeraChem alone while the wavefunction
preferred-ESS order does not list TeraChem; Level(method='rhf', basis='cc-pvdz',
software='terachem') builds and reaches this gate, so the entry is live rather than dead.
… in output.yml Three additions to the per-species output entry, all written from parsed logs. wavefunction_stability, the human-readable summary string the end-of-run report prints, plus the structured block behind it. _parse_wavefunction_stability reads the stability log and records the verdict, the negative stability-matrix eigenvector labels and their eigenvalues, the lowest eigenvalue, the reference the analysis ran on, and invalidates_analytic_freq, together with the run-relative path of the log it came from. It is not recomputed here: the parser derives the verdict and its consequence once, and this entry reads that single result. scf_reference, the provenance block. It records source (declared / derived / null), declared_number_of_radicals, verdict, verdict_restricted, sp_reference, freq_reference, reference_mismatch and the stability log it was read from -- flat, so that arcbench's _spin_diagnostic_payload allow-list is unaffected. reference_mismatch is null, NOT false, when either reference is unknown. A job carrying no reference memo leaves nothing to compare, and publishing that as false is indistinguishable from two references checked and found to agree. Every sibling key in the block uses null for unknown; this one does too. log names the analysis a 'derived' source was decided from even after the stability path is gone. A TS switch resets that path while the species keeps the verdict it carried across, so reading the path alone published a source of 'derived' beside a null log -- a decision with nothing behind it. The block falls back to the log path the species' own verdict carries, made run-relative like the other one. The block is NOT gated on convergence, unlike the parsed quantities beside it and like the wavefunction_stability entry it explains. It records a decision ARC made rather than a number a job produced, and a species that adopted a verdict and then failed to converge is precisely the case where knowing ARC changed its reference explains the failure. Gating it hid that record exactly when it was wanted. A well's verdict reaches output.yml as verdict / verdict_restricted with source null, which the entry's is_ts separates from a TS's identical verdict reading 'derived'. No new key is added for that: the pair already says it, and a column computed from its neighbours is a liability. sp_spin_diagnostic, the <S**2> block, in the same shape and key names as the arcbench emitter that consumes it, so the two bind unchanged. Its fallback loop now stops at the first candidate path that EXISTS, not at the first that yields a value. Continuing past a log that exists but yields no <S**2> is not what the fallback was written for: an sp on Molpro or CFOUR, neither of which implements parse_s_squared and both of which therefore inherit the base class's None, together with opt and freq on Gaussian UHF, populated sp_spin_diagnostic from the FREQ log at a different level of theory while d['sp_log'] still named the Molpro file -- and arcbench's TCKDB adapter uploads that block as the sp calculation's <S**2>. A present-but-silent log now ends the search and the block is omitted, and the block records the run-relative path of the log it was actually read from under 'log', following the precedent _parse_wavefunction_stability sets at the same call site. The new key is additive and safe for the arcbench consumer, which copies an allow-list out of the block and ignores every other key. Verified by mutation. Reading a non-dict scf_references with .get fails 1; publishing an unknown reference_mismatch as false, 1; gating the provenance block on convergence, 1; never reporting reference_mismatch, 1; never naming a source in the provenance block, 3; recording the stability block for a TS only, 2; reversing the sp/freq/opt fallback order, 2; and dropping the `if converged else None` gate on sp_spin_diagnostic, 1. _parse_wavefunction_stability's documented return schema was missing keys it already emitted. n_analyses and followed_to_stable reach output.yml and are now documented, along with the new s_squared_after_follow and the 'unattributed_instability' verdict. The note also records which wavefunction each field describes: verdict, lowest_eigenvalue, negative_eigenvectors and restricted belong to the wavefunction under TEST, while the energies and spin values the published log also holds belong to the FOLLOWED solution the ESS relaxed into. Nothing consumes the followed energy today, but output.yml publishes that log's path, so a future consumer would read the wrong wavefunction off it in silence.
…rdict, carry it across a TS switch
The scheduler side of the wavefunction-stability diagnostic: where the analysis sits in a
species' job sequence, what an adopted verdict does to that sequence, and what survives a TS
guess switch.
SEQUENCING. The analysis is a single point, so it can run the moment the optimization
converges, and that is where spawn_post_opt_jobs runs it:
opt -> stability -> freq / sp / IRC / rotors, or
opt -> stability -> opt (unrestricted) -> freq / sp / IRC / rotors
Nothing else is enqueued from the post-opt path while the analysis is out. The frequency
job, the single point, the IRC and the rotor scans all inherit the SCF reference and the
geometry of the optimization, so a reference that is not the ground state is caught before a
Hessian and an energy are spent on it, and before a re-optimization would have to throw them
away. The optimization job's name is written to the species as stability_pending_opt_job
BEFORE the analysis is spawned, and spawn_post_stability_jobs re-enters spawn_post_opt_jobs
with it; the analysis runs at most once per species, so the re-entry spawns none and falls
through. That resume is reached for every stability job that leaves running_jobs -- converged,
errored, or holding a log no verdict could be read from -- so an analysis that produces
nothing releases the species rather than stranding it.
RE-OPTIMIZATION IS WHAT MAKES AN ADOPTION CORRECT. Adopting a verdict without re-optimizing
would leave the geometry a stationary point of the RESTRICTED surface while the Hessian is
built on the broken-symmetry reference, which is a Hessian at a non-stationary point and can
report imaginary modes belonging to the mismatch rather than to the molecule. So an adopted
verdict re-runs the opt at the opt level, from the geometry the first optimization reached,
and is_species_restricted reads the adopted verdict off the species, so that job and every
job after it run unrestricted. E0 is then E_elect and ZPE from one surface.
AT MOST ONE RE-OPTIMIZATION PER SPECIES. ARCSpecies carries stability_analysis_ran,
stability_pending_opt_job and stability_reoptimized, all serialised through as_dict /
from_dict, so a run resumed between the analysis and the re-optimization spawns neither a
second analysis nor a second optimization. schedule_jobs releases any species holding a
pending optimization with no analysis of its own still queued, which is the state a run
resumed after its analysis ended leaves behind: the job it was waiting on is gone, so nothing
else would reach the resume for it.
THE ORBITALS THE RE-OPTIMIZATION STARTS FROM depend on what the ESS did with the instability
it found, which the verdict reports as followed_to_stable. ORCA runs STABPerform with
STABRestartUHFifUnstable, follows the instability, and writes the relaxed orbitals to the
analysis job's input.gbw; those are the broken-symmetry solution the re-optimization is meant
to sit on, so the species adopts them. Gaussian's stable=(rext,noopt) reports an instability
without following it, so its checkfile still holds the restricted orbitals, and handing those
to an unrestricted SCF returns it to the very solution the analysis rejected -- a restricted
solution is a stationary point of the unrestricted equations too. The checkfile is dropped in
that case and the job runs guess=mix, whose deliberately symmetry-broken guess is what finds
the lower solution.
SPAWNING GUARDS. Gaussian and ORCA only, DFT or HF only, the species must still hold the
checkfile its own optimization wrote, at most one analysis per species, and an early return
on job_types['stability'], which ships False, so a run that does not ask for this changes in
no way. The admission gate is `is_ts or job_scf_reference_is_restricted(opt_job) is True` --
a TS always, and any other species whose optimization actually ran on a restricted reference,
since an already-unrestricted job has nothing to learn from the test. A job that is not a
submitted ESS job carries none of this and is refused, so a pipe task releases the species
rather than holding it.
Every guard reports why it declined, including the ESS one. A job that ran in an ESS ARC has
no reader for is refused with a warning naming the species and that ESS, and saying that ARC
implements the analysis for Gaussian and ORCA only -- not that the ESS cannot perform one,
which would send the user looking in the wrong place. It is a warning rather than an info
line because the other refusals are per-job conditions that leave the feature working
elsewhere in the same run, whereas this one means the job type the user switched on will
never run for any species that ESS handles. It is emitted once per ESS per run, recorded in
Scheduler.stability_unimplemented_ess, since the condition is a property of the run and not
of the species that happened to reach it first; a 200-species project therefore gets one
line, not 200, and a mixed-ESS project one line per ESS.
CONSUMING. check_stability_job parses the verdict, writes it to the species and to
output.yml, and logs it: a warning naming the negative root and its eigenvalue for an
instability, an info line for a stable verdict. A restricted external instability does not
set invalidates_analytic_freq -- but it is not merely 'a statement about the restricted
description, not about the Hessian'. A Hessian built from that description inherits its
error: it is a correct second derivative of the surface that was computed, but that surface
is not the ground state, and near an RHF -> UHF instability onset the restricted surface is
spuriously stiff along the bond-stretching coordinate, which for a TS is the reaction
coordinate, so the imaginary frequency and the barrier curvature are wrong in a known
direction, too large and too high. Running the analysis before the Hessian is what keeps that
Hessian from being computed at all.
A campaign scan of every geometry-deduplicated restricted-singlet TS, 56 in all, found 12
unstable across 4 of 19 reactions, eigenvalues from -0.015 to -0.064 Hartree. Every one is
an RHF -> UHF instability with a triplet negative root; the scan turned up no singlet
negative eigenvalue at all. That scan is also why no guess is rejected on the verdict:
within one reaction the six unstable geometries share an eigenvalue to seven decimals, so
they are one stationary point found six ways, and rejecting would bias rather than filter --
in one reaction the three lowest-energy saddles are the unstable ones and the only stable one
is the highest, while in another the unstable ones sit 61 kcal/mol above the stable set.
log_open_shell_character_sources reports, per species, what was measured and whether ARC is
acting on it. A well found unstable is told in words that ARC is NOT acting on it, why not,
and what to declare to act on it. That last part names `number_of_radicals = 2`, not "a
number_of_radicals": the code reads a declaration as open-shell character only ABOVE one, so
a user who followed generic advice literally with 1 would get a restricted reference and
silence.
THE PER-JOB REFERENCE RECORDS remain, and their subject is narrower than the sequencing
above. record_scf_reference files each completed job's own restricted_used memo under
species.scf_references, so it reports what ran rather than what would run now.
SCF_REFERENCE_JOB_TYPES maps a job type onto the two terms of an E0: 'sp' supplies the
energy, and 'freq' OR the combined 'optfreq' supplies the ZPE. optfreq is not an
afterthought -- a guard listing only ['sp', 'freq'] silently drops every combined job, so a
species optimised and differentiated in one job records no ZPE reference at all and can never
report a mismatch. Reference-agnostic levels are not recorded, because comparing a CBS-QB3 sp
against a uwB97XD freq would report a mismatch that does not exist.
check_scf_reference_consistency raises a logger.warning naming both references and spelling
out E0 = E_elect(<sp>) + ZPE(<freq>), plus a persistent entry in the species' output
warnings. What it catches is a pair of jobs composed on either side of some other change to
the species' state: an sp resubmitted by troubleshooting, an sp held past its freq, or a
species restored from a restart. Its docstring says so rather than describing an adoption as
its subject, since a species that adopts a verdict re-optimizes and runs both terms on one
reference.
A STALE RECORD WAS A REAL SOURCE OF FALSE POSITIVES, and is fixed here. post_freq_actions
returns (True, switch_ts): freq_ok is True EVEN WHEN it switched the TS guess, and
check_freq_job recorded the reference one statement after switch_ts had deliberately cleared
scf_references. The abandoned guess' {'freq': 'restricted'} went straight back in, the next
guess' sp then recorded 'unrestricted' against it, and the mismatch warning fired for a guess
whose freq had in fact run unrestricted -- landing in output[label]['warnings'], which
delete_all_species_jobs does not reset. check_freq_job records only when the geometry
survived the check.
THE TS SWITCH CARRY RULE. carry_stability_verdict_across_ts_switch keeps an ADOPTED external
instability and drops everything else. Keeping it is NOT justified by any claim that an
instability is a property of the reaction rather than of one saddle -- the campaign data
refutes that: two of the four reactions carrying instabilities have MIXED verdicts across
distinct saddles. What justifies keeping it is that carrying it is FREE when it does not
apply. Variationally E(UKS) <= E(RKS) with equality if and only if the restricted solution is
stable, so if the next guess is in fact stable, forcing it unrestricted returns exactly the
restricted energy and costs SCF effort, not accuracy. What it buys is that the next guess is
unrestricted from its very FIRST optimization, which is the reference the discovering guess
reaches only by being optimized twice: a carried verdict spares the next guess that second
optimization and the analysis that would have prompted it. The pending optimization is
released at the same point, since switch_ts abandons that job along with the geometry it
converged to.
What is dropped in every case is the geometry-specific detail -- the negative eigenvector
labels and eigenvalues, the relaxed constraints, the lowest eigenvalue and
invalidates_analytic_freq all describe the abandoned wavefunction and its Hessian, and no
measurement of them exists for the new guess, since stability_analysis_ran stays set across
a switch. A carried verdict keeps verdict and restricted, plus measured_on_ts_guess
naming the guess it came from and the path of the analysis log it was read from, both of
which output.yml reports. 'stable', 'unknown', an internal
instability and an external instability of an unrestricted reference are dropped outright:
none decides a reference, and carrying one would attribute a bill of health to a geometry
never tested. So is a verdict ARC would not adopt because a number_of_radicals was declared
-- the declaration decides the reference, and carrying the verdict would promise the next
guess a change that is not coming.
species.scf_references is cleared outright at a switch, and so is the mixed-reference warning
they raised: opt, freq and sp all re-run for the new guess, so the abandoned guess' per-job
records describe nothing. The invalid-Hessian and spin-contamination warnings go with them,
for the same reason and about the same jobs.
THE TWO RECORDS OF A SWITCHED-AWAY GUESS ARE REDUCED TOGETHER. output[label]
['wavefunction_stability'] and the sentence the verdict added to output[label]['info'] are
top-level keys that nothing cleared, while delete_all_species_jobs resets
output[label]['paths'] at the same switch. So output.yml reported no verdict for the
surviving geometry -- it reads the path -- while the end-of-run summary printed the abandoned
guess' summary string against it, down to the negative root: 'external_instability
(Triplet-A, -0.0642)', naming a geometry that no longer exists. Both are cleared where the
species' own verdict is reduced, so the two records say the same thing about the same
geometry.
THE CALL ORDER INSIDE switch_ts IS NOW PINNED. measured_on_ts_guess is read off
species.chosen_ts, so carry_stability_verdict_across_ts_switch has to run BEFORE
determine_most_likely_ts_conformer picks the replacement, or the carried verdict is
attributed to the guess that had not been measured. Swapping the two left the suite green:
the test covering the path mocked determine_most_likely_ts_conformer out entirely, so
chosen_ts never moved and the ordering was invisible. The replacement lets the selection
actually change chosen_ts and asserts the carried verdict still names the abandoned one,
which fails when the two calls are swapped.
THE MIXED-REFERENCE CHECK NO LONGER MISSES THE MOST COMMON CONFIGURATION. record_scf_reference
was reachable from check_freq_job and check_sp_job only, and when the sp level equals the opt
level run_sp_job takes its equal-level branch and calls post_sp_actions directly -- no sp job
is submitted and check_sp_job is never reached. So scf_references['sp'] was never written for
a single-level run, check_scf_reference_consistency returned at its first guard forever, and
reference_mismatch was permanently null. The recording moves into post_sp_actions, which is
the one point both paths pass through, and takes the job whose log the energy is actually read
from: the sp job where one ran, the optimization job where none did. record_scf_reference
takes the key explicitly for that case, since an opt job's type does not say which term of the
E0 it supplied.
AN INVALIDATED ANALYTIC HESSIAN REACHES THE USER. invalidates_analytic_freq was parsed,
recorded and logged, and then reached no output the user reads: the mixed-reference case wrote
its message into output[label]['warnings'], which is what carries a warning into output.yml
and the run summary, while the invalid-Hessian case wrote only to ['info'].
INVALID_ANALYTIC_FREQ_MESSAGE now goes to ['warnings'] alongside it. No job flow changes:
nothing is re-run, re-referenced or invalidated, and the frequencies, the ZPE built from them
and the E0 built from that are reported as computed, with the warning attached.
SPIN CONTAMINATION IS SURFACED WHERE THE ENERGY IS READ. <S**2> was measured and published in
output.yml and compared against nothing: a doublet TS at 1.7488 against a spin-pure 0.75, 133%
contamination, passed as 'stable' with invalidates_analytic_freq False and its E0 went to
Arkane in silence. check_spin_contamination reads the diagnostic off the log the electronic
energy came from and warns, in the log and in the species' output warnings, when the deviation
from the spin-pure S(S+1) exceeds MAX_S_SQUARED_DEVIATION = 0.1. The threshold is an ABSOLUTE
deviation, not a fraction of the spin-pure value, because a singlet's spin-pure value is zero
and the broken-symmetry singlet is exactly the case that most needs reporting. Its size
follows from what a deviation means: the nearest contaminant of a state of spin S is the state
of spin S+1, whose S(S+1) lies 2S+2, at least 2, above it, so 0.1 is at most a five percent
admixture. A converged doublet at 0.7536 and a triplet at 2.0086 stay silent; an adopted
broken-symmetry singlet does not, which is the point. A restricted reference prints no <S**2>
and an ESS with no reader for it reports none, and both are passed over rather than reported
uncontaminated. Job flow is unchanged here too.
A VERDICT IS ACTED ON ONLY WHERE EVERY ADAPTER COMPOSING A TERM OF THE SPECIES' E0 WRITES
A SYMMETRY-BROKEN REFERENCE. stability_verdict_can_be_honoured tests the optimization,
which supplies the geometry, the frequency job, which supplies the ZPE, and the single
point, which supplies the electronic energy, against SYMMETRY_BREAKING_ADAPTERS, skipping
a reference-agnostic level, which adoption does not change what is composed for. Acting
on a verdict with only some of those adapters writing such a reference moves the geometry
and the Hessian onto the broken-symmetry surface and leaves the energy on the restricted
one, so the published E0 sums terms from two surfaces and belongs to neither, and the job
that composed no symmetry-broken reference records an unrestricted memo for an SCF that
converged to the restricted solution, which check_scf_reference_consistency then reads as
agreement. Such a verdict is recorded, logged, and reported in the species' output
warnings as UNREACHABLE_REFERENCE_MESSAGE, and decides nothing. A run whose optimization,
frequency job and single point are all composed by one of those adapters is unaffected.
THE TWO SETS ARE STATEMENTS ABOUT ARC'S ADAPTERS AND NOT ABOUT WHAT THE ESSs CAN DO, and
are named and documented as such: STABILITY_CAPABLE_ESS and SYMMETRY_BREAKING_CAPABLE_ESS
become STABILITY_ANALYSIS_ADAPTERS and SYMMETRY_BREAKING_ADAPTERS, with a module-level
docstring saying what each holds. Molpro is the case the distinction matters for: Molpro
has a {uhf} program and takes a ROTATE directive that mixes two starting orbitals, which
is how a broken-symmetry singlet is requested of it, while ARC's Molpro adapter writes
{hf} in every input it composes and spends the unrestricted decision on the u prefix of
the correlation method. The log messages and the output warning say that an adapter writes
no symmetry-broken reference rather than that an ESS offers none.
A DROPPED VERDICT CLEARS stability_analysis_ran, so the TS guess that survives a switch
is measured in its turn. The dropped verdict describes a wavefunction that is gone and
the next guess is a different saddle, so leaving the flag set would publish that guess'
restricted energy with no verdict of its own and nothing to distinguish a guess never
measured from one measured stable. A CARRIED verdict keeps the flag set: its reference is
already decided, and a fresh analysis of the unrestricted reference the next guess runs on
measures a different question than the one that was adopted.
A STABILITY JOB THAT DIED WITH ITS ANALYSIS ALREADY PRINTED IS READ FOR THE VERDICT IT
PRINTED. The analysis precedes whatever killed the job, so its blocks are complete or
absent rather than truncated into a different verdict, and the log is parsed whenever it
exists rather than only where the job status is 'done'. Nothing is troubleshooted or
re-run on it.
UNREACHABLE_REFERENCE_MESSAGE IS STRIPPED ON A TS SWITCH with the other four. The method's own
docstring says the two records are reduced together, and this warning was the one left behind: it
outlived the guess it described while output[label]['wavefunction_stability'] was set to None on
the same pass. It is always safe to strip, and always right to: it is raised only on a verdict
stamped REFERENCE_CHANGE_AVAILABLE_KEY False, which adopted_reference_is_unrestricted reads as
well, so such a verdict is never one this method carries over -- it is dropped here along with the
geometry it was measured on. The next guess is measured in its turn and raises the warning again
where its own verdict cannot be honoured.
post_sp_actions' docstring now says why its restart-path caller passes no job, rather than leaving
the guard reading as an oversight. That branch runs only where sp_level == opt_level, where one
job supplied both the geometry and the energy and the two share one SCF reference by construction,
so the reference comparison the record feeds has nothing to find; what it costs is a null rather
than a false reference_mismatch in output.yml for a restarted single-level project.
The one added inline comment in spawn_post_opt_jobs is dropped: the method's docstring already
states, at length, that the analysis is the single job spawned there and that everything else
waits on its verdict.
The ORCA stability job emits %moinp "guess.gbw" and uploads guess.gbw to the remote job path, but every ORCA template in this file copied only the input file into the scratch $WorkDir, and `grep -n gbw arc/settings/submit.py` returned nothing at all. With ARC's repo defaults ORCA therefore aborts with `Cannot open file guess.gbw` on every stability job, determine_ess_status classifies that as errored/['Unknown'], and ARC troubleshoots a deterministic failure for as long as the run lasts. The feature only worked on the machine it was developed on because that machine's ~/.arc/submit.py overlay happens to glob *.gbw, and a repo settings value says nothing about production: the overlay shallow-replaces whole dicts, so what a developer runs and what this branch ships are different files. Every ORCA template now copies guess.gbw in and input.gbw back out, in each template's own style: the three that name the files they return copy both explicitly, the two that copy the whole work directory back need only the inbound line, and the HTCondor job.sh lists input.gbw beside the input.log and input_property.txt it already names. Both copies are tolerant of an absent file -- most jobs hand ORCA no guess, and a job that died may have written no orbitals -- so a missing file writes nothing to err.txt and does not affect the exit status. This mirrors the Gaussian template, which has copied check.chk in for as long as guess=read has been emitted.
Psi4 is the one ESS adapter that does not route its construction through _initialize_adapter, so the guard JobAdapter.readable_checkfile applies to every other adapter did not reach it. Scheduler hands every job the checkfile its species holds whichever ESS wrote it, and this adapter assigned it unconditionally and then uploaded it as check.chk, so a species optimized in ORCA would have had its input.gbw handed to Psi4 under a Gaussian name. The assignment now goes through the same guard, which is a one-expression change and leaves every other line of the adapter alone.
TeraChem writes its converged orbitals to teracheck.chk, while the JobAdapter defaults name Gaussian's check.chk. The base name is what readable_checkfile identifies the ESS that wrote a checkfile by, and what set_file_paths builds local_path_to_check_file from, so the adapter accepted a Gaussian checkfile as an initial guess and refused TeraChem's own, and the file a completed TeraChem job leaves behind was looked for under a name TeraChem never writes. check_file_name and guess_file_name are declared on TeraChemAdapter as they are on OrcaAdapter, and the two places the name was spelled out in the adapter, the fallback to an orbitals file in the job's own directory and the chkfile line of the input template, read the class attribute. The fallback goes through readable_checkfile, so it refuses an empty file as every other adapter's does.
Adds ``stability`` to the job type list in the advanced documentation, and to the two
job-type dictionaries the page shows as examples, so that a reader copying either gets a
dictionary matching the one ARC now builds.
The entry states the whole user-visible contract in one place: the job runs in Gaussian and
ORCA and is off by default; it runs from the optimization, ahead of the frequency job, the
single point, the IRC and the rotor scans, all of which are held for its verdict; it runs
for a TS and for any other species whose optimization ran with a restricted reference, which
is the only reference it can inform; a TS's external instability of a restricted reference
re-optimizes the species unrestricted from the geometry the first optimization reached, at
most once per species and recorded in the restart file; a declared ``number_of_radicals``
always wins; and for anything that is not a TS the verdict is reported and nothing acts on
it.
It also states which orbitals the re-optimization starts from and why the answer differs by
ESS: ORCA follows the instability and writes the relaxed broken-symmetry orbitals, which are
the ones to start from, while Gaussian's ``stable=(rext,noopt)`` leaves its checkfile holding
the restricted solution, which an unrestricted SCF reading it would simply return to, so the
checkfile is dropped and the job runs ``guess=mix``.
It also says what a long run of ``stable`` verdicts means, so that it is read as the
diagnostic working rather than as it having nothing to do: a well verified stable has
identical restricted and unrestricted energies, so a barrier taken between it and a TS ARC
has made unrestricted is a difference on one surface rather than across two, and an
undeclared singlet biradical is caught by nothing else in ARC.
And it says what an adopted verdict does not buy. The broken-symmetry solution is not a spin
eigenfunction; it mixes in the higher multiplicity, so its energy lies ABOVE the spin-pure
low-spin energy, and the restricted energy it replaces lies above the broken-symmetry one in
turn: E_projected < E_BS < E_restricted. An adoption is therefore a step toward the spin-pure
energy that stops short of it. ARC does not project the contamination out; ``arc/checks/spin.py``
holds the Yamaguchi arithmetic that estimates E_projected, so the residual error after an
adoption is the contamination itself, in the direction it already had. The size of that
residual is not left to be inferred either: an ``<S**2>`` deviating from its spin-pure S(S+1)
by more than 0.1 is warned about where the electronic energy is read.
The advice to declare ``number_of_radicals = 2`` names the value rather than the key, because
ARC reads a declaration as open-shell character only above one.
Also documents the ORCA path: why the instability is always followed rather than only
reported, that the resulting log holds two analyses and which one is the verdict, how the
orbitals under test are handed over given that ORCA names its own orbitals after the
input file, and why an ORCA stable verdict on an unrestricted reference says less than a
Gaussian one.
Corrects the claim that the two codes do not test the same space. They do: Gaussian's
Stable=RExt uses the same Ms-conserving <AA,BB:AA,BB> singles matrix ORCA uses, for both
references, and neither reaches the GHF sector, so neither verdict is the weaker one. The
comparison that produced the original claim was also run at unmatched functional -- plain
ORCA B3LYP is VWN-5 against Gaussian's VWN3 -- and at matched functional the roots agree to
under 0.4% wherever both codes found the same SCF solution. The one system that still
disagrees is a near-dissociated radical pair on which the two codes converged to different
UHF solutions, and it supports no cross-code conclusion. The section also now records how a
restricted reference's sector is measured from the followed solution's <S**2>, that a
barrier taken across an adopted verdict is OVERestimated in a known direction by the TS's
residual contamination -- less so than the all-restricted barrier it replaces, which sat
higher still -- that neither code computes a spin-flip root for an unrestricted reference so
both readers leave the external sector undetermined there, that both ESSs
chain their orbitals from the optimization so the tested wavefunction is the
optimization's, which ORCA job types read a guess and what the chain is worth on a
broken-symmetry TS, and that a site running its own submit.py must copy guess.gbw in and
input.gbw out.
Also documents that a verdict is acted on only where the optimization, the frequency job
and the single point all run in an ESS ARC breaks the spin symmetry for, which the
default Molpro single point is not; that a TS switch drops every verdict it does not
carry and lets the next guess be measured; that the ORCA sector is read off any solution
the log relaxed into rather than only a stable one; and that a single point batched
through the pipe composes the same reference, the verdict travelling with the species
dictionary the pipe task carries.
The paragraph on an ESS ARC can offer neither mechanism is rewritten as a statement about
ARC's adapters: Molpro has a {uhf} program and a ROTATE directive that mixes two starting
orbitals, while ARC's Molpro adapter writes {hf} in every input it composes, so what the
run cannot do is compose the reference rather than ask Molpro for it.
THE BS-UHF CCSD(T)-F12 MEASUREMENT IS WITHDRAWN. The three rows behind it were internally
consistent -- E_corr equalled E_CC minus E_SCF to machine precision for each -- but they refute one
another. Against the ROHF triplet row, another open-shell F12 treatment of the same molecule, same
basis, same code family, the BS-UHF reference recovered 0.2237 Eh less correlation, 22.7 per cent
of the total; against RHF, 0.3264 Eh, 30 per cent. The RHF and ROHF-triplet total energies agree to
0.011 Eh, 6.9 kcal/mol, which is what a total energy should do once correlation is included; the BS
row sits 140 kcal/mol above RHF and 133 above ROHF. Two independent treatments agreeing to 7
kcal/mol and a third disagreeing by 140 is a broken calculation, not a spin-relaxation effect, and
the direction is wrong as well: BS-UHF is variationally 64 kcal/mol BELOW RHF at the SCF level and
is the better zeroth-order description of a biradicaloid, so coupled cluster built on it cannot
land 140 kcal/mol above. The prose also summarised its own table incorrectly, quoting 0.35 Eh where
the numbers give 0.3264.
The mechanism is Molpro's: ccsd(t)-f12 is its closed-shell program and uccsd(t)-f12 its open-shell
one over ROHF orbitals, the U naming spin-unrestricted amplitudes rather than unrestricted
orbitals. Neither takes a spin-broken UHF determinant as its reference, so orbitals from
{uhf; rotate,...} give an expansion about something that is not the BS determinant, and
E_CC minus E_SCF(BS-UHF) then subtracts two different references' energies.
The T1 and D1 figures measured on that same BS run are withdrawn with it: a diagnostic read off a
calculation that is not an energy of the state is no more trustworthy than the energy. The
argument is made on the ROHF triplet row instead, where it is made more cleanly anyway -- T1 falls
from 0.0410 to 0.0118, below the 0.015 at which ARC reports multireference character, while the
total energy moves under 7 kcal/mol. Spin relaxation of the reference suppresses the diagnostic
with none of the weirdness, which is exactly the point the section needed.
The section also documents the double-hybrid exclusion and the HF-3c admission, and the run-summary
claim it makes about warnings is now true of arc/main.py.
input_reference.rst had no mention of `stability` at all, and its job_types example listed six keys out of twelve, so the only place the feature was documented was advanced.rst -- which describes behaviour and is not where a user goes to find out which keys an input file accepts. The feature was therefore discoverable only by reading the settings module. `stability` is added to the job type key list and to the example with its default, and the example now states which keys default to true and which to false, since the example itself lists a mixture of the two and previously implied that the ones it omitted were unavailable rather than defaulted. A short section states what the analysis is, that ARC has implemented it for Gaussian so far and that other ESSs are not wired up yet, when it runs (once per species, after that species' freq job, for a TS or for a species whose freq job actually ran restricted, at a DFT or HF level, with the freq job's checkfile), and what the user gets back. Which instabilities invalidate analytic frequencies, and what an adopted verdict does and does not correct, stay in advanced.rst; this section links there rather than restating them. The `specific_job_type` interaction is documented because it is a trap. That key replaces job_types wholesale with a dictionary in which only the named type is true, so `specific_job_type: stability` is accepted -- stability is a key of default_job_types, so no InputError is raised -- and then produces nothing at all, because opt, freq and sp are all false and run_stability_job is only ever reached from post_freq_actions. bde is special-cased to re-enable opt/fine/freq/sp; stability is not. Verified against initialize_job_types rather than inferred: specific_job_type 'stability' yields opt False, freq False, sp False, stability True. The reference now says to request it through job_types and says why. Deliberately not documented here: which other programs implement a stability analysis and under what keyword. ORCA and Q-Chem both do, but their exact syntax was not verified against their manuals for this commit, and an unverified keyword in ARC's documentation is worse than none. The text is kept to what ARC does. Also records that the job type now runs in ORCA as well as in Gaussian, that ORCA always follows an instability it finds, and that the two codes test the same space and agree on the verdict, with the sector of a restricted reference's instability read off the solution ORCA relaxes into.
20a0f2a to
800cc8c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 43 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
docs/source/input_reference.rst:268
- This paragraph still frames the stability job as consuming the checkfile a frequency job used and a Hessian that was already built. Since stability runs before the frequency calculation, this should be phrased in terms of what the frequency job will use/build so the timeline is accurate.
| It runs once per species, after that species' frequency job, and only for a transition | ||
| state or for a species whose frequency job actually ran with a restricted reference - a | ||
| restricted reference is the only one the analysis can inform, since a restricted solution | ||
| gives the same energy as an unrestricted one if and only if it is stable. It is further |
Teaches ARC to test whether the SCF reference it chose is actually the ground state, and to act on the answer. Gaussian and ORCA.
Off by default (
'stability': Falseinsettings.py) — a run that does not ask for it is unchanged.What it adds
An opt-in
stabilityjob type, spawned once per species when its optimisation converges — for every TS, and for any other species whose opt actually ran restricted, which is the only reference the analysis can inform. The analysis is a single point, so it needs no frequencies.The optimisation's tail — freq, sp, IRC, rotors and the rest — is held until the verdict is in, because each of those inherits the optimisation's reference or its geometry:
An instability means the geometry is wrong too: it is a stationary point of the restricted surface only. So an adopted verdict re-optimises rather than merely re-referencing — a Hessian taken at the restricted geometry on an unrestricted reference sits at a non-stationary point and can produce spurious imaginary modes. At most one re-optimisation per species, guarded by
stability_reoptimizedonARCSpeciesand carried inrestart.yml, and a resumed run releases any species whose analysis finished while it was down.The re-optimisation's starting orbitals are per-ESS, because the two codes leave different things behind. ORCA follows the instability and writes the relaxed broken-symmetry orbitals, which seed the re-optimisation. Gaussian's
stable=(rext,noopt)does not follow, so its checkfile still holds the restricted orbitals — and a restricted determinant is a stationary point of the unrestricted equations, so reading it back converges to the solution the analysis rejected. That case drops the checkfile instead and takes Gaussian'sguess=mix.stable=(rext,noopt)at the freq level on the freq geometry.STABPerform true/STABRestartUHFifUnstable true, plus.gbworbital tracking.A parser reads the verdict, the negative stability-matrix roots and their eigenvalues, and derives whether the analytic frequencies are invalidated. An unreadable verdict reports
unknown, neverstable.Alongside it, the
<S**2>spin-contamination diagnostic is re-homed from an arcbench-based branch ontomain(Gaussian, ORCA, Q-Chem), andarc/checks/spin.pygains the Yamaguchi approximate spin-projection arithmetic.The adoption contract
number_of_radicalsalways wins and is never overwritten by a calculation.An internal instability never flips the reference — it is a lower solution inside the reference's own spin symmetry, which is a different problem and not evidence of broken-symmetry character.
The derived verdict lives in its own
ARCSpecies.derived_stability_verdict, deliberately not innumber_of_radicals, which feeds molecular-graph perception in eight places plus xTB's UHF count. A measured SCF property must not decide which molecule ARC thinks it has.Measurement widened to every restricted species; adoption stays TS-only. A well is never re-optimised on its verdict, so adopting for one would guarantee the
E_elect(unrestricted) + ZPE(restricted)splice a TS only risks. Because adoption is TS-only, a TS carries a broken-symmetry geometry and ZPE while its reactants and products do not. Yamaguchi projection givesE_projected < E_BS < E_restricted, so a broken-symmetry energy sits above the spin-pure one it approximates and the barrier the run reports is systematically overestimated by the residual contamination of the TS alone — less so than the all-restricted barrier it replaces. That direction is stated in the docstring and inadvanced.rst.Adoption governs DFT except double hybrids, and Hartree-Fock including HF-3c. A verdict measured at the optimisation level decides the reference for the geometry and the ZPE, and does not reach a correlated single point. Measured on the same TS in Molpro at cc-pVDZ:
The argument rests on the triplet ROHF row: spin relaxation drops
T1from 0.0410 to 0.0118, below the 0.015 threshold at which ARC reports a multireference species, because the orbitals absorb the static correlation the diagnostic detects — while the total energy moves under 7 kcal/mol. The diagnostic that exists to catch a multireference species stops catching it, at no cost in the energy.A broken-symmetry coupled-cluster row was withdrawn after review, and no BS-UHF correlated energy is quoted because none exists. Molpro's
ccsd(t)-f12is the closed-shell program anduccsd(t)-f12the ROHF-orbital open-shell one — the "U" names spin-unrestricted amplitudes, not orbitals — so neither takes a genuinely spin-broken UHF determinant as its reference, and the difference that was reported subtracted two different references' energies. The arithmetic gave it away before the mechanism did: the row implied the BS reference recovers 0.22 Eh less correlation than the triplet ROHF row, another open-shell F12 treatment of the same molecule in the same basis, while being variationally 64 kcal/mol below RHF at the SCF level. ItsT1/D1are withdrawn with it, since a diagnostic read off a calculation that is not an energy of the state is worth no more than the energy.level_admits_a_broken_symmetry_reference()reads aDOUBLE_HYBRID_METHODSdeny-list first, then gates onBROKEN_SYMMETRY_METHOD_TYPES = ['dft']andBROKEN_SYMMETRY_METHODS = ['hf', 'hf3c', 'rhf', 'uhf', 'rohf'], rather than by exempting'wavefunction'—hfcarries that method type, and an exempt level makesjob_scf_reference_is_restrictedreturnNone, which would stop the single point being recorded at all. Double hybrids are excluded because a double hybrid's energy is not its determinant's energy: it carries an MP2 term expanded about the KS determinant, which is the same construction that keeps a correlated wavefunction level on its restricted reference, and BS-MP2 about a spin-broken reference is the pathology this gate exists to prevent.E0 therefore takes its geometry and ZPE from one reference and its electronic energy from another.
check_scf_reference_consistencyreports that:output.ymlrecordsreference_mismatch: truewith the mixed-reference message, andT1stays loud.Provenance — source, declared count, verdict, per-job-type SCF references, mismatch flag and the log it was read from — is recorded in
output.ymland survives a restart. A TS switch carries an adopted external instability forward, dropping everything geometry-specific.The ORCA path, and what had to be measured to write it
ORCA's manual documents the input keywords but not the output, so the parser could not be written from documentation. Thirteen ORCA 6.0.0 jobs were run (five fixtures, four diagnostics, four functional-matched controls) on the four geometries the Gaussian fixtures already use.
Verdict wording, which ORCA's manual does not publish:
STABRestartUHFifUnstable trueis mandatory, not a preference. With itfalse, ORCA 6.0.0 terminates inLEANSCFwith a BLAS incompatible-matrices error (exit 62) after printing a complete verdict — reproduced atnprocs 8andnprocs 1, atSTABNRoots6 and 3;LeanSCF falsefails earlier still. The three stable jobs terminate normally. ORCA therefore cannot do Gaussian'snoopt"report but do not follow", and ARC always tells it to follow.A followed log holds two analysis blocks with opposite verdicts.
verdictis taken from the first — the wavefunction under test. The second describes what ORCA relaxed into and is exposed separately asfollowed_to_stable. Gaussian'snooptlogs never exercise this.On the unstable singlet TS, following the instability gained −0.02022 Eh (−12.7 kcal/mol) and took
<S**2>from0.000000to0.864742— a genuine open-shell singlet whose restricted energy was wrong by 12.7 kcal/mol..gbwtrackingORCA's analysis is an SCF post-step: it converges an SCF first, so without reading the tested orbitals it may converge to a different solution — the hazard Gaussian's checkfile requirement exists to prevent. ARC had no
.gbworMOReadhandling.checkfilewas already ESS-generic (psi_4→check.chk,terachem→teracheck.chk), so this rides existing plumbing viacheck_file_name/guess_file_nameclass attributes onJobAdapter. ORCA names its own output after the input file, so it cannot read and write one.gbwthe way Gaussian reuses one.chk: the guess is uploaded asguess.gbwand read with!MORead/%moinp, while the job's owninput.gbwis what returns.Every ORCA job that runs an SCF on a single structure reads the guess when one exists —
opt,conf_opt,optfreq,scan,freq,sp,conf_sp,stability— mirroring Gaussian'sguess=read, which sits outside any job-type branch. Job types for which the adapter writes no calculation keyword, monatomic species, and job arrays (whose members share one remote path) read none. The emission set and the upload set are the same predicate,OrcaAdapter.reads_orbital_guess(), and a test asserts they agree across all 15 job types.This is what keeps the chain consistent. Measured on the C₅H₁₀ TS — identical
!UKS B3LYP def2-TZVPinput, same geometry, only the guess differing:<S**2>!MORead12.7 kcal/mol. The fresh guess collapses onto the closed-shell solution, which the stability analysis reports unstable at that geometry (lowest root −0.0647); reading the optimisation's orbitals holds the stable broken-symmetry solution. A basis change between jobs is handled by ORCA itself — a
def2-SVPjob reading adef2-TZVP.gbwlogsN(Shells)= 6 and 11 - projection requiredand converges — so no level tracking is required.The sector is measured, not assumed
ORCA prints one unlabelled stability matrix, which for a restricted reference spans both the internal (singlet) and external (R→U triplet) sectors. Assuming external would drive reference adoption and suppress the analytic-frequency warning on evidence the log does not contain.
Because ARC is forced to run the follow anyway, the post-restart
<S**2>is free and decides it: a nominal singlet reaching a stable solution aboveSPIN_SYMMETRY_BREAKING_S_SQUARED = 0.01proves the spin symmetry broke. Where the follow never converged, a new verdictunattributed_instabilityis reported with both flagsNone— never a fabricated external, and it deliberately does not trigger reference adoption.Cross-code validation
ORCA analyses RHF/RKS in UHF/UKS space and UHF/UKS in UHF/UKS space. Gaussian's
Stable=RExtuses the same Ms-conserving block — all four Gaussian fixtures printStability analysis using <AA,BB:AA,BB> singles matrix:— so the two tests span the same space and neither reaches the GHF sector.The two codes agree on the verdict for all four systems. Comparing eigenvalues requires a matched functional, since ORCA's
B3LYPuses VWN-5 and Gaussian's uses VWN3; ORCA's matching keyword isB3LYP/G:Total energies agree to 0.0002 Eh, so the functional accounted for the entire raw offset. The outlier is not a code difference: at matched functional its energies still differ by 0.025 Eh with
<S**2>1.7488 vs 1.700055, because the two codes converged to different UHF solutions of a near-dissociated O(³P)···CH₃ pair at r(O–C) = 3.78 Å. No cross-code conclusion is drawn from that system.What review changed
A shipping blocker.
arc/job/adapters/orca.pyemitted%moinp "guess.gbw"and uploaded the file, but no ORCA template inarc/settings/submit.pycopied it into the scratch working directory — Gaussian hascp "$SubmitDir/check.chk" .; every ORCA template copied onlyinput.in. With repo defaults any ORCA job reading a guess would abort with "Cannot open file guess.gbw", be classifiederrored/['Unknown'], and be retried forever. It passed development testing only because that cluster's~/.arc/submit.pyoverlay happens to glob*.gbw. All six templates now copyguess.gbwin andinput.gbwout; all 36 templates were re-checked to still.format().Two silent-corruption paths. A failed
.gbwdownload leaves a 0-byte file (paramiko opens the local file before the remote), which passedos.path.isfile()and was adopted asspecies.checkfile; size is now checked. And an ORCA.gbwcould be handed to Gaussian ascheck.chkwithguess=read, reachable wheneveropt_levelroutes to ORCA and a later job to Gaussian — adapters now refuse a checkfile that is not theirs, viaJobAdapter.readable_checkfile().A test-ordering bug of the kind that has been destabilising this suite. Five tests asserted
record.levelname == 'WARNING', butarc.common.initialize_logcallsaddLevelName(logging.WARNING, 'Warning: '). They pass alone and fail whenever anything initialises ARC's log first — which-n 6 --dist workstealcan arrange. They comparelevelnonow.Earlier rounds: the Gaussian pass refuted the premise the work started from (g16 does not ignore
guess=mixon a restricted reference —Mixing orbitals ... Coef= 7.07106781D-01on a closed-shell singlet, systematic across 24 fixtures). The chemistry pass caught the Yamaguchi arithmetic being silently singlet-only, hard-coding<S²>_LS = 0— a 16.8 kcal/mol error for a BS-doublet/HS-quartet pair. The adversarial pass measured the separation guard returning −101 Ha from a −1.0 Ha reference; the floor is now physical rather than a divide-by-zero guard, and NaN/inf are rejected.before-annihilationis the<S**2>fed to the projection, because that is the value the SCF energy belongs to — the annihilated one biases it +21.2 kcal/mol.Discoverability
run_stability_jobrefuses an unsupported ESS with a warning naming the species and the ESS, emitted once per ESS per run. It is a warning rather than an info because it means the job type the user explicitly switched on will never run for any species that ESS handles — a configuration mismatch, not a situational skip — and once per ESS so a 200-species project gets one line rather than 200.docs/source/input_reference.rstlistsstabilityamong the job-type keys and in thejob_typesexample, and states which keys default true and which false.Verification
12 commits, no file touched by more than one. Commit order is bisect-safe:
readable_checkfilesits inarc/job/adapter.pyso the adapter commits can use it without a forward dependency.Full suite, serial, rebased onto current
main: 3214 passed, 43 skipped, 5 failed. The 5 arearc/job/adapters/torch_ani_test.py, failing on an unconfiguredTANI_PYTHON, which isNonelocally; nothing here touches that adapter.🤖 Generated with Claude Code