Skip to content

fix: guard against a None species.thermo in parse_arkane_thermo_output - #995

Open
alongd wants to merge 1 commit into
mainfrom
arkane-thermo-none-guard
Open

fix: guard against a None species.thermo in parse_arkane_thermo_output#995
alongd wants to merge 1 commit into
mainfrom
arkane-thermo-none-guard

Conversation

@alongd

@alongd alongd commented Aug 17, 2026

Copy link
Copy Markdown
Member

Crash

Real T3 campaign run (~/runs/t3-pes-CH2O2/) died after all QM jobs converged, during thermo
reporting:

AttributeError: 'NoneType' object has no attribute 'H298' and no __dict__ for setting new attributes
  File "arc/statmech/arkane.py", line 509, in parse_arkane_thermo_output
    spc.thermo.H298 = content[lbl]['H298']

Root cause

ARCSpecies.__init__ always sets self.thermo = ThermoData() as a default. Somewhere between
species construction and this loop, a species can end up with spc.thermo is None again — I could
not pin the exact assignment site that clears it in the live run, but the codebase itself already
treats this as an expected, guarded-against state in three independent places:

  • ArkaneAdapter.set_reaction_dh_rxn (arc/statmech/arkane.py)
  • processor.process_arc_project (arc/processor.py:198)
  • output.get_species_output_dict (arc/output.py:510)

parse_arkane_thermo_output's result-assignment loop was the one place that assumed spc.thermo
can never be None and crashed instead of guarding — classifying this as case (a): a species
legitimately reaching this code with no thermo container, not a valid_labels filtering bug (I
found no evidence valid_labels includes species it should exclude).

Fix

Restore the ThermoData() default (matching the exact construction used in ARCSpecies.__init__)
immediately before populating it with the real, successfully-computed Arkane results, instead of
crashing and discarding those results:

if spc.thermo is None:
    spc.thermo = ThermoData()
spc.thermo.H298 = content[lbl]['H298']
...

Sibling kinetics path

Checked parse_arkane_kinetics_output/parse_reaction_kinetics for the same unguarded-attribute
pattern: none found. parse_reaction_kinetics always does one wholesale reaction.kinetics = kinetics dict assignment rather than mutating attributes on an existing kinetics sub-object, so
there's no equivalent None-target crash risk there.

Containment at the process_arc_project/compute_thermo boundary

Deliberately did not add a broader try/except there. The crash's root data problem is now fixed
at its source (the offending species gets a real ThermoData container populated with its actual
results, not skipped). A blanket guard at the reporting boundary would risk swallowing genuinely
new data-integrity bugs in future runs rather than surfacing them — better handled as its own
decision if a distinct need for it shows up.

Testing

Added test_parse_arkane_thermo_output_recovers_missing_thermo_container to
arc/statmech/arkane_test.py, covering a species with .thermo is None reaching the loop and a
well-formed sibling species processed normally in the same call.

  • Baseline (git stash, unmodified branch): 44 passed, 2 failed. The 2 failures
    (test_generate_arkane_input, test_run_statmech_using_molecular_properties) are pre-existing,
    caused by a missing arkane module in the rmg_env conda environment ARC shells out to — unrelated
    to this change.
  • Post-fix: 45 passed, 2 failed (same 2 pre-existing failures, +1 new passing test).
  • Verified the new test is a genuine regression test: reverted only arc/statmech/arkane.py to
    HEAD and re-ran it in isolation — it failed with the exact same traceback signature as the real
    crash above, then passed again after reapplying the fix.

Comment thread arc/statmech/arkane_test.py Fixed
@alongd

alongd commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Root cause found; PR updated. Two corrections to what this PR originally claimed.

1. The "restore the __init__ default" framing was wrong. git log -S "self.thermo = ThermoData()" -- arc/species/species.py returns exactly one commit (b2be26dc8, "Implement ThermoData and TransportData classes in species"). The line has never been removed. Nothing was restored, because nothing was dropped.

2. The None does not originate in ARC at all. I could not find the producer when opening this PR, and said so. It is now located, and the reason the search failed is that it is on the other side of the boundary:

  • ARCSpecies.__init__ sets self.thermo = ThermoData() unconditionally (arc/species/species.py:362);
  • from_dict never touches the attribute, and it is not serialised into restart.yml;
  • no ARC production code assigns None to it — grep -rn "thermo\s*=\s*None" arc/ --include=*.py matches only test files.

The producer is a caller's subclass. T3's T3Species(ARCSpecies) ran self.thermo = thermo after super().__init__(), with thermo defaulting to None, so every species T3 handed ARC arrived with the invariant already broken. Fixed at the source in ReactionMechanismGenerator/T3#187.

This PR is still worth merging as defence in depth, and I've kept it. ARC should not discard converged QM results because a caller violated an invariant ARC never advertised — and this loop was the odd one out, since set_reaction_dh_rxn, process_arc_project and get_species_output_dict all already tolerate a None thermo.

Changes since the first push:

  • commit message rewritten to say what actually happens, and to name the real producer;
  • the guard now emits a logger.warning instead of repairing silently. A silent guard means the next caller to re-introduce this gets absorbed with no trace, which is how the state stayed unexplained this long.

Tests unchanged: 45 passed, 2 pre-existing failures (test_generate_arkane_input, test_run_statmech_using_molecular_properties) that are the known rmg_env missing-arkane environment issue, identical to the baseline on main.

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.45%. Comparing base (7ae26d3) to head (5c2eb25).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #995      +/-   ##
==========================================
- Coverage   64.46%   64.45%   -0.01%     
==========================================
  Files         119      119              
  Lines       39636    39639       +3     
  Branches    10276    10276              
==========================================
- Hits        25550    25549       -1     
- Misses      11102    11106       +4     
  Partials     2984     2984              
Flag Coverage Δ
functionaltests 64.45% <ø> (-0.01%) ⬇️
unittests 64.45% <ø> (-0.01%) ⬇️

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.

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.

Pull request overview

This PR prevents a crash in ArkaneAdapter.parse_arkane_thermo_output when a species unexpectedly reaches thermo-result assignment with spc.thermo is None, by restoring a ThermoData() container before populating Arkane results, and adds a regression test covering that scenario.

Changes:

  • Guard parse_arkane_thermo_output against spc.thermo is None by instantiating ThermoData() before assignment.
  • Add a regression unit test ensuring parsing succeeds and populates thermo for both a None-thermo species and a normal species.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
arc/statmech/arkane.py Adds a None guard for spc.thermo during Arkane thermo result assignment and logs a warning when recovering.
arc/statmech/arkane_test.py Adds a regression test that reproduces the crash condition and verifies thermo assignment still succeeds.

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

Comment thread arc/statmech/arkane_test.py
Comment thread arc/statmech/arkane.py
@alongd
alongd force-pushed the arkane-thermo-none-guard branch from b702b7d to ec910be Compare August 19, 2026 20:42
@alongd
alongd requested a lite review from Copilot August 20, 2026 21:04

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

arc/statmech/arkane_test.py:166

  • The fixture writes an empty output.py, which deliberately bypasses the earlier parse_species_thermo() path. Consequently this test reaches the new YAML-assignment guard, but it would not catch the same None crash for the normal Arkane output that contains a ThermoData(...) block; include such a block so the regression test exercises the full method path.
        with open(os.path.join(statmech_dir, 'output.py'), 'w') as f:
            f.write('')

arc/statmech/arkane.py:515

  • This guard is reached only after parse_species_thermo() has already run for every species at line 483. For a normal Arkane output.py containing a thermo(..., ThermoData(...)) block, that helper calls species.thermo.update(...) at line 1266, so a None container still raises before this new branch and the reported crash is not prevented. Initialize the container before the parse loop or add the same guard inside parse_species_thermo() before calling update().
                    if spc.thermo is None:
                        # ``ARCSpecies.__init__`` sets ``self.thermo = ThermoData()`` unconditionally,
                        # so no species ARC builds itself can reach this loop with ``None``. A caller's
                        # subclass can, though, by re-assigning the attribute after ``super().__init__()``
                        # -- which is exactly how T3's ``T3Species`` produced the crash this guards

``parse_arkane_thermo_output`` assigns straight through the attribute::

    spc.thermo.H298 = content[lbl]['H298']

with no None check, and crashes with

    AttributeError: 'NoneType' object has no attribute 'H298'

when a species reaches that loop with ``.thermo`` set to None. This fires at
the *reporting* stage, after every QM job in the run has already converged --
so the cost is an entire successful computation discarded at the last step.

No ARC code produces that state. ``ARCSpecies.__init__`` sets
``self.thermo = ThermoData()`` unconditionally, ``from_dict`` never touches the
attribute, and it is not serialised into the restart file. The None arrives
from a *caller's subclass* re-assigning the attribute after
``super().__init__()``; T3's ``T3Species`` did exactly that, and is fixed at the
source in T3 PR #187.

Guard here anyway, as defence in depth: ARC should not lose converged results
because a caller violated an invariant it never advertised. Other call sites
(``ArkaneAdapter.set_reaction_dh_rxn``, ``processor.process_arc_project``,
``output.get_species_output_dict``) already tolerate a None thermo, so this
loop was the odd one out.

The guard warns rather than repairing silently, so a caller re-introducing the
state stays visible instead of being absorbed.

Real-run crash (traceback in the PR body): arc/statmech/arkane.py:509, inside
process_arc_project's first (non-e0_only) compute_thermo() call.
@alongd
alongd force-pushed the arkane-thermo-none-guard branch from 2d6366f to 5c2eb25 Compare August 22, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants