Skip to content

Fix bi-rad singlet composite jobs silently running a restricted reference - #1055

Open
alongd wants to merge 2 commits into
mainfrom
birad-singlet-composite
Open

alongd wants to merge 2 commits into
mainfrom
birad-singlet-composite

Conversation

@alongd

@alongd alongd commented Sep 13, 2026

Copy link
Copy Markdown
Member

What

is_species_restricted() returned early for REFERENCE_AGNOSTIC_METHOD_TYPES (composite,
force-field, semiempirical) before reading number_of_radicals, so the bi-rad-singlet branch
below it was unreachable for exactly those method types. Move the number_of_radicals > 1 check
ahead of that early return.

Why

An open-shell singlet biradical is declared in ARC as an ARCSpecies with multiplicity=1 and
number_of_radicals=2 — that is what the ARCSpecies docstring prescribes, and it is the only way
to express the state. The existing code already knew what to do with it; its own comment says so:

# don't run unrestricted for composite methods such as CBS-QB3, it'll be done automatically if the
# multiplicity is greater than one, but do specify uCBS-QB3 for example for bi-rad singlets.

That comment describes a case the function could never reach. Composite methods take
method_type == 'composite', so G4/CBS-QB3 returned True (restricted) at the top, and the
branch the comment documents never ran. A declared bi-rad singlet therefore went out as plain g4
with a restricted reference — no u prefix — and converged to the wrong electronic state
without warning. Nothing errors; the thermochemistry is simply wrong.

The asymmetry is the whole point: for a composite method an unrestricted reference is applied
automatically when multiplicity > 1, but a bi-rad singlet's multiplicity is 1, so the method
gets no signal and ARC has to say uG4 explicitly.

What changed

arc/job/adapters/common.pynumber_of_radicals > 1 is now evaluated first and returns
unrestricted, then the reference-agnostic early return, then the multiplicity > 1 case. The
existing # don't run unrestricted for composite methods... comment is replaced by one that
explains why the order matters, so the next reader does not "simplify" it back.

Unaffected: plain closed-shell singlets (number_of_radicals unset or 1) and composite jobs at
multiplicity > 1 both take exactly the paths they did before.

Testing

arc/job/adapters/common_test.py — 61 passed, with new cases covering a composite bi-rad singlet
(unrestricted), a composite closed-shell singlet (restricted), and a composite triplet (unchanged).

Verified end to end on the route section: a g4 job for a species with multiplicity: 1, number_of_radicals: 2 now writes ug4.

is_species_restricted() returned early for composite/force_field/
semiempirical method types before ever checking number_of_radicals,
making the bi-rad-singlet branch (multiplicity 1, number_of_radicals > 1)
unreachable for those method types. A G4/CBS-QB3 job for a bi-rad singlet
therefore ran as a restricted reference with no 'u' prefix and no
guess=mix effect, silently converging to the wrong electronic state.

Move the number_of_radicals > 1 check ahead of the reference-agnostic
early return so a declared bi-rad singlet is always run unrestricted,
including for composite methods (uG4, uCBS-QB3). Plain closed-shell
singlets and multiplicity > 1 composite jobs are unaffected.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Preserve force-field reference-agnostic handling before approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes unrestricted-reference selection for composite bi-radical singlet jobs.

Changes:

  • Prioritizes number_of_radicals > 1.
  • Adds regression coverage for composite and Gaussian route generation.
File summaries
File Summary
arc/job/adapters/common.py Reorders reference selection; a critical finding notes this can produce uuff for force-field jobs.
arc/job/adapters/common_test.py Adds composite biradical, closed-shell, triplet, and route-generation tests.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

logger.info(f'Using an unrestricted method for species {species_label} which has '
f'{number_of_radicals} radicals and multiplicity {multiplicity}.')

if number_of_radicals is not None and number_of_radicals > 1:
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.18%. Comparing base (575d216) to head (51f93ce).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1055      +/-   ##
==========================================
- Coverage   66.26%   66.18%   -0.09%     
==========================================
  Files         122      122              
  Lines       41824    41827       +3     
  Branches    10751    10751              
==========================================
- Hits        27716    27682      -34     
- Misses      11062    11090      +28     
- Partials     3046     3055       +9     
Flag Coverage Δ
functionaltests 66.18% <ø> (-0.09%) ⬇️
unittests 66.18% <ø> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alongd
alongd requested a review from calvinp0 September 15, 2026 02:44
ARCSpecies.__init__ and ARCSpecies.from_dict disagreed on the multiplicity
of the same species. __init__ reads multiplicity and charge off the molecule
built from the adjlist/InChI/SMILES, and only afterwards may replace .mol
with one perceived from the coordinates (guarded by regen_mol/keep_mol).
from_dict ran that override first, unguarded, and then read the multiplicity
off the perceived molecule. The two differ by statement order alone.

Perception picks an electronic state, and for an open-shell species it
routinely picks a different one from the structure that was given:
'C[C]C#N' (multiplicity 3) perceives from its own optimized coordinates as
'C[C][C][N]' (multiplicity 1). Since a deck travels the from_dict path, a
species declared open-shell could run its whole job closed-shell -- the job
converges, terminates normally and reports a plausible result for a
different electronic state. Same failure shape as the restricted-reference
defect this branch already fixes, reached by a different route.

Capture multiplicity and charge from the structure-derived molecule before
mol_from_xyz() replaces it, and fall back to the perceived molecule only
when no structure was given at all. An explicitly declared multiplicity
still wins, as before.
@alongd

alongd commented Sep 20, 2026

Copy link
Copy Markdown
Member Author

Added a second, related fix to this branch (51f93ce), since it is the same failure — a job that runs the wrong electronic state and reports it as a normal result — reached by a different route.

ARCSpecies.__init__ and ARCSpecies.from_dict disagree on the multiplicity of the same species:

xyz = "<the optimized geometry of C[C]C#N>"
ARCSpecies(label="s", smiles="C[C]C#N", xyz=xyz).multiplicity                      # 3
ARCSpecies(species_dict={"label": "s", "smiles": "C[C]C#N", "xyz": xyz}).multiplicity  # 1

__init__ reads multiplicity/charge off the molecule built from the SMILES/adjlist/InChI (species.py:507-508) and only then may overwrite .mol from the coordinates, guarded by regen_mol and not keep_mol. from_dict runs mol_from_xyz() first with no guard (1011-1014) and reads the multiplicity afterwards (1024-1025). The two differ by statement order alone. Perception picks an electronic state, and for an open-shell species it routinely picks a different one: C[C]C#N perceives from its own optimized coordinates as C[C][C][N].

A deck travels the from_dict path, so this is not hypothetical — it silently ran 13 of 31 open-shell species in a controlled A/B study at the wrong multiplicity. The jobs converged and terminated normally.

The fix captures multiplicity and charge from the structure-derived molecule before the override, falling back to the perceived molecule only when no structure was given at all. An explicitly declared multiplicity still wins. arc/species/ + arc/main_test.py + arc/job/adapters/common_test.py run serially: 446 passed, 1 failure (main_test.py::TestARC::test_as_dict) that is present on this branch before the change as well.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants