Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions t3/pdep/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,21 @@ def _call_keywords(call: ast.Call, path: str = '', call_name: Optional[str] = No
# these calls. Mapping positions to names would mean duplicating Arkane's signatures here and
# keeping them in step with it forever, which is a bigger promise than refusing a spelling nothing
# writes.
#
# THE ONE EXCEPTION -- ``transitionState(label, path)``. Arkane's ``transitionState(label, *args,
# **kwargs)`` (arkane/input.py:241) loads a stat-mech file from a path ONLY through its
# ``len(args) == 1 and len(kwargs) == 0`` branch, i.e. the two-positional-argument spelling
# ``transitionState('TS2', 'qm/TS2.py')``; there is NO keyword spelling for it
# (``transitionState(label='TS2', path='qm/TS2.py')`` silently stops loading the file). T3's own
# hybrid writer (``t3/pdep/hybrid.py``) must therefore emit exactly that positional form when it
# vendors a QM-adopted transition state, and this reader -- the same file's other consumer -- has to
# read it back, so refusing it crashes T3 on a file T3 itself wrote. Map that single form (label,
# path) to keywords and nothing wider: any OTHER positional shape (one, or three-plus, positionals;
# positionals mixed with keywords) and this same two-positional form for any OTHER call name still
# fall through to the refusal below. The corpus survey the refusal rests on never contained T3's own
# writer's output, which is why this producer/consumer disagreement was invisible until a real run.
if call_name == 'transitionState' and len(call.args) == 2 and not call.keywords:
return {'label': call.args[0], 'path': call.args[1]}
Comment on lines +1336 to +1337
if call.args:
raise ValueError(f"The pdep network file at '{path}' calls "
f"'{call_name or _get_call_name(call) or '<unnamed>'}(...)' with "
Expand Down
21 changes: 21 additions & 0 deletions tests/data/pdep_hybrid/positional_ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# `positional_ts/network0_reduced.py`

A byte-for-byte copy (sha256
`46c152d162e789fcb003d4a79af57fa96eec6a07ca5e0df69757855ea4f43d96`) of the hybrid network file the
PES exploration loop wrote in round 0 of the `r001_m1-cho2-pilot` run and then crashed reading in
round 1. T3's own hybrid writer (`t3/pdep/hybrid.py`) produced it; the copy is vendored here because
the run tree under `/home/alon/runs/` is scratch and will be pruned.

Its load-bearing line is:

```python
transitionState('TS2', 'qm/TS2.py')
```

TS2 was adopted from quantum chemistry, so the writer rewrites it to the two-positional
`transitionState(label, path)` spelling — the ONLY form Arkane (`arkane/input.py:241`,
`if len(args) == 1 and len(kwargs) == 0`) accepts for loading a stat-mech file from a path; there is
no keyword spelling for it. The pdep parser's `_call_keywords` refused every positional argument and
crashed on this file. `tests/test_pdep/test_parser.py::TestPositionalTransitionState` drives this
file through `parse_pdep_network_file` and asserts the parser recovers both TS2's label and its path.
Do not regenerate or "clean up" this file: its value is that it is exactly what a real run emitted.
166 changes: 166 additions & 0 deletions tests/data/pdep_hybrid/positional_ts/network0_reduced.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
modelChemistry = LevelOfTheory(method='wb97xd2023',basis='def2tzvp',software='gaussian')
frequencyScaleFactor = 0.988
useHinderedRotors = True
useBondCorrections = False
atomEnergies = {'Br': -2574.174533595486, 'C': -37.84706210301937, 'Cl': -460.1467876783656, 'F': -99.73955550924293, 'H': -0.5006557872395249, 'N': -54.584995947182875, 'O': -75.07252406126821, 'S': -398.1105530401693}
useAtomCorrections = True

species(
label = 'O=[C]O(8)',
structure = adjacencyList("""multiplicity 2
1 O u0 p2 c0 {3,S} {4,S}
2 O u0 p2 c0 {3,D}
3 C u1 p0 c0 {1,S} {2,D}
4 H u0 p0 c0 {1,S}
"""),
E0 = (-184.487,'kJ/mol'),
modes = [
HarmonicOscillator(frequencies=([1855,455,950,607.278,1696.14],'cm^-1')),
HinderedRotor(inertia=(0.000457043,'amu*angstrom^2'), symmetry=1, barrier=(0.119627,'kJ/mol'), semiclassical=False),
],
spinMultiplicity = 2,
opticalIsomers = 1,
molecularWeight = (45.0174,'amu'),
collisionModel = TransportData(shapeIndex=2, epsilon=(4140.61,'J/mol'), sigma=(3.59,'angstroms'), dipoleMoment=(0,'De'), polarizability=(0,'angstroms^3'), rotrelaxcollnum=2.0, comment="""NOx2018"""),
energyTransferModel = SingleExponentialDown(alpha0=(3.5886,'kJ/mol'), T0=(300,'K'), n=0.85),
thermo = NASA(polynomials=[NASAPolynomial(coeffs=[3.65821,0.00206077,2.03288e-05,-2.91283e-08,1.14311e-11,-22171.5,8.26011], Tmin=(100,'K'), Tmax=(964.009,'K')), NASAPolynomial(coeffs=[7.09694,0.0016118,-4.75818e-07,1.29874e-10,-1.30513e-14,-23476.6,-11.5343], Tmin=(964.009,'K'), Tmax=(5000,'K'))], Tmin=(100,'K'), Tmax=(5000,'K'), E0=(-184.487,'kJ/mol'), Cp0=(33.2579,'J/(mol*K)'), CpInf=(78.9875,'J/(mol*K)'), comment="""Thermo group additivity estimation: group(O2s-(Cds-O2d)H) + group(Cds-OdOsH) + radical((O)CJOH)"""),
)

species(
label = '[O]C=O(1)',
structure = adjacencyList("""multiplicity 2
1 O u1 p2 c0 {3,S}
2 O u0 p2 c0 {3,D}
3 C u0 p0 c0 {1,S} {2,D} {4,S}
4 H u0 p0 c0 {3,S}
"""),
E0 = (-169.935,'kJ/mol'),
modes = [
HarmonicOscillator(frequencies=([2782.5,750,1395,475,1775,1000],'cm^-1')),
],
spinMultiplicity = 2,
opticalIsomers = 1,
molecularWeight = (45.0174,'amu'),
collisionModel = TransportData(shapeIndex=2, epsilon=(4140.61,'J/mol'), sigma=(3.59,'angstroms'), dipoleMoment=(0,'De'), polarizability=(0,'angstroms^3'), rotrelaxcollnum=2.0, comment="""NOx2018"""),
energyTransferModel = SingleExponentialDown(alpha0=(3.5886,'kJ/mol'), T0=(300,'K'), n=0.85),
thermo = NASA(polynomials=[NASAPolynomial(coeffs=[3.9649,-0.00518829,3.73952e-05,-4.21919e-08,1.47224e-11,-20431.8,7.33591], Tmin=(100,'K'), Tmax=(994.515,'K')), NASAPolynomial(coeffs=[5.26528,0.00541264,-2.4716e-06,5.38764e-10,-4.28526e-14,-21473.4,-2.86661], Tmin=(994.515,'K'), Tmax=(5000,'K'))], Tmin=(100,'K'), Tmax=(5000,'K'), E0=(-169.935,'kJ/mol'), Cp0=(33.2579,'J/(mol*K)'), CpInf=(83.1447,'J/(mol*K)'), comment="""Thermo group additivity estimation: group(O2s-(Cds-O2d)H) + group(Cds-OdOsH) + radical(OJC=O)"""),
)

species(
label = '[H](6)',
structure = adjacencyList("""multiplicity 2
1 H u1 p0 c0
"""),
E0 = (211.805,'kJ/mol'),
spinMultiplicity = 1,
opticalIsomers = 1,
molecularWeight = (1.00797,'amu'),
energyTransferModel = SingleExponentialDown(alpha0=(3.5886,'kJ/mol'), T0=(300,'K'), n=0.85),
thermo = NASA(polynomials=[NASAPolynomial(coeffs=[2.5,-3.01681e-12,3.74582e-15,-1.50857e-18,1.86626e-22,25474.2,-0.444973], Tmin=(100,'K'), Tmax=(4879.8,'K')), NASAPolynomial(coeffs=[4.28461,-0.00145495,4.44804e-07,-6.0436e-11,3.07922e-15,23723.1,-11.8931], Tmin=(4879.8,'K'), Tmax=(5000,'K'))], Tmin=(100,'K'), Tmax=(5000,'K'), E0=(211.805,'kJ/mol'), Cp0=(20.7862,'J/(mol*K)'), CpInf=(20.7862,'J/(mol*K)'), label="""H""", comment="""Thermo library: primaryThermoLibrary"""),
)

species(
label = 'O=C=O(5)',
structure = adjacencyList("""1 O u0 p2 c0 {3,D}
2 O u0 p2 c0 {3,D}
3 C u0 p0 c0 {1,D} {2,D}
"""),
E0 = (-403.087,'kJ/mol'),
modes = [
HarmonicOscillator(frequencies=([459.922,1087.69,1087.69,2296.71],'cm^-1')),
],
spinMultiplicity = 1,
opticalIsomers = 1,
molecularWeight = (44.0094,'amu'),
energyTransferModel = SingleExponentialDown(alpha0=(3.5886,'kJ/mol'), T0=(300,'K'), n=0.85),
thermo = NASA(polynomials=[NASAPolynomial(coeffs=[3.27862,0.00274142,7.16109e-06,-1.08032e-08,4.14302e-12,-48470.3,5.97934], Tmin=(100,'K'), Tmax=(988.879,'K')), NASAPolynomial(coeffs=[4.54606,0.00291919,-1.15487e-06,2.27661e-10,-1.70916e-14,-48980.3,-1.43257], Tmin=(988.879,'K'), Tmax=(5000,'K'))], Tmin=(100,'K'), Tmax=(5000,'K'), E0=(-403.087,'kJ/mol'), Cp0=(29.1007,'J/(mol*K)'), CpInf=(62.3585,'J/(mol*K)'), comment="""Thermo group additivity estimation: missing(O2d-Cdd) + missing(O2d-Cdd) + group(Cdd-OdOd)"""),
)

species(
label = 'Ar',
structure = adjacencyList("""1 Ar u0 p4 c0
"""),
molecularWeight = (39.8775,'amu'),
collisionModel = TransportData(shapeIndex=0, epsilon=(1134.93,'J/mol'), sigma=(3.33,'angstroms'), dipoleMoment=(0,'De'), polarizability=(0,'angstroms^3'), rotrelaxcollnum=0.0, comment="""NOx2018"""),
energyTransferModel = SingleExponentialDown(alpha0=(3.5886,'kJ/mol'), T0=(300,'K'), n=0.85),
thermo = NASA(polynomials=[NASAPolynomial(coeffs=[2.5,0,0,0,0,-745.375,4.37967], Tmin=(200,'K'), Tmax=(1000,'K')), NASAPolynomial(coeffs=[2.5,0,0,0,0,-745.375,4.37967], Tmin=(1000,'K'), Tmax=(6000,'K'))], Tmin=(200,'K'), Tmax=(6000,'K'), E0=(-6.19738,'kJ/mol'), Cp0=(20.7862,'J/(mol*K)'), CpInf=(20.7862,'J/(mol*K)'), label="""Ar""", comment="""Thermo library: primaryThermoLibrary"""),
)

transitionState(
label = 'TS1',
E0 = (21.347 - 191.282, "kJ/mol"), # removing the applied energy_correction
spinMultiplicity = 1,
opticalIsomers = 1,
)

transitionState('TS2', 'qm/TS2.py')

transitionState(
label = 'TS3',
E0 = (30.706 - 191.282, "kJ/mol"), # removing the applied energy_correction
spinMultiplicity = 1,
opticalIsomers = 1,
)

reaction(
label = 'reaction3',
reactants = ['[H](6)', 'O=C=O(5)'],
products = ['[O]C=O(1)'],
transitionState = 'TS1',
kinetics = Arrhenius(A=(7699.21,'m^3/(mol*s)'), n=1.33054, Ea=(21.3474,'kJ/mol'), T0=(1,'K'), comment="""Estimated using template [Cdd_Od;HJ] for rate rule [CO2;HJ]
Euclidian distance = 1.0
family: R_Addition_MultipleBond
Ea raised from 17.5 to 21.3 kJ/mol to match endothermicity of reaction."""),
)

reaction(
label = 'reaction5',
reactants = ['[O]C=O(1)'],
products = ['O=[C]O(8)'],
transitionState = 'TS2',
tunneling = 'Eckart',
)

reaction(
label = 'reaction8',
reactants = ['[H](6)', 'O=C=O(5)'],
products = ['O=[C]O(8)'],
transitionState = 'TS3',
kinetics = Arrhenius(A=(2.37e+08,'cm^3/(mol*s)'), n=1.63, Ea=(30.7064,'kJ/mol'), T0=(1,'K'), Tmin=(300,'K'), Tmax=(1700,'K'), comment="""Estimated using template [Od_R;HJ] for rate rule [Od_Cdd-O2d;HJ]
Euclidian distance = 2.0
Multiplied by reaction path degeneracy 2.0
family: R_Addition_MultipleBond"""),
)

network(
label = 'PDepNetwork #1',
isomers = [
'O=[C]O(8)',
'[O]C=O(1)',
],
reactants = [
],
bathGas = {
'Ar': 1,
},
)

pressureDependence(
label = 'PDepNetwork #1',
Tmin = (700,'K'),
Tmax = (3200,'K'),
Tcount = 10,
Tlist = ([3200,2290.91,1784.07,1460.87,1236.81,1072.34,946.479,847.059,766.54,700],'K'),
Pmin = (0.1,'bar'),
Pmax = (100,'bar'),
Pcount = 10,
Plist = ([0.1,0.215443,0.464159,1,2.15443,4.64159,10,21.5443,46.4159,100],'bar'),
maximumGrainSize = (2,'kJ/mol'),
minimumGrainCount = 250,
method = 'modified strong collision',
interpolationModel = ('pdeparrhenius',),
activeKRotor = True,
activeJRotor = True,
rmgmode = True,
)

73 changes: 73 additions & 0 deletions tests/test_pdep/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,3 +1348,76 @@ def test_end_to_end_a_real_energy_corrected_ts_yields_a_relative_e0(self):
assert ts.label == 'TS1'
assert ts.relative_e0 is not None
assert ts.relative_e0 == pytest.approx((264.497 - 411.735) - 0.0)


# The hybrid network file the PES loop wrote in round 0 of the r001_m1-cho2-pilot run and then
# crashed reading in round 1. Vendored byte-for-byte because /home/alon/runs/ is scratch; see
# tests/data/pdep_hybrid/positional_ts/README.md for provenance and the sha256.
HYBRID_POSITIONAL_TS_FILE = os.path.join(
TEST_DATA_BASE_PATH, 'pdep_hybrid', 'positional_ts', 'network0_reduced.py'
)


def _transition_state_call(text: str, label: str) -> ast.Call:
"""Return the top-level ``transitionState(...)`` call whose FIRST positional argument is the
string literal ``label``, from real pdep-network file text. Used to hand ``_call_keywords`` the
exact AST node a real file produced, rather than one a test hand-built."""
for node in ast.parse(text).body:
if not (isinstance(node, ast.Expr) and isinstance(node.value, ast.Call)):
continue
call = node.value
if (isinstance(call.func, ast.Name) and call.func.id == 'transitionState'
and call.args and isinstance(call.args[0], ast.Constant)
and call.args[0].value == label):
return call
raise AssertionError(f"no positional transitionState({label!r}, ...) call in the text")


class TestPositionalTransitionState:
"""The producer/consumer bug I-016: T3's hybrid writer emits ``transitionState('TS2',
'qm/TS2.py')`` -- the two-positional form Arkane (arkane/input.py:241) requires to load a
stat-mech file from a path, and the ONLY form it offers for that -- while the pdep reader
refused every positional argument and crashed on the file T3 itself had just written."""

def test_real_hybrid_file_parses(self):
"""Verifier 1: the real crashed artifact drives through the pdep parser and parses."""
network = parse_pdep_network_file(HYBRID_POSITIONAL_TS_FILE)
# Verifier 2a -- the label is recovered through the full public parse, not merely
# "no exception raised": TS2 (the QM-adopted, positionally-written state) is present
# alongside the keyword-written TS1 and TS3.
assert 'TS2' in network.transition_state_labels
assert set(network.transition_state_labels) == {'TS1', 'TS2', 'TS3'}

def test_real_hybrid_file_recovers_ts2_label_and_path(self):
"""Verifier 2b: the parse recovers BOTH the transition state's label AND its path from the
positional call, rather than silently discarding the path a keyword reader never mapped."""
with open(HYBRID_POSITIONAL_TS_FILE, 'r') as f:
text = f.read()
call = _transition_state_call(text, 'TS2')
kwargs = _call_keywords(call, path=HYBRID_POSITIONAL_TS_FILE, call_name='transitionState')
assert set(kwargs) == {'label', 'path'}
assert ast.literal_eval(kwargs['label']) == 'TS2'
assert ast.literal_eval(kwargs['path']) == 'qm/TS2.py'

def test_positional_exception_is_narrow_other_call_names_still_refused(self):
"""Non-goal guard: the widening is exactly ``transitionState(label, path)``. The same
two-positional shape for any OTHER call name still falls through to the refusal."""
(call,) = [n.value for n in ast.parse("species('X', 'qm/X.py')").body]
with pytest.raises(ValueError, match='POSITIONAL'):
_call_keywords(call, call_name='species')

def test_positional_exception_is_narrow_other_arities_still_refused(self):
"""Non-goal guard: only two positionals map. One, or three, positional argument(s) to a
``transitionState(...)`` call is not the Arkane path form and is still refused."""
for src in ("transitionState('TS2')",
"transitionState('TS2', 'qm/TS2.py', 'extra')"):
(call,) = [n.value for n in ast.parse(src).body]
with pytest.raises(ValueError, match='POSITIONAL'):
_call_keywords(call, call_name='transitionState')

def test_positional_exception_is_narrow_mixed_positional_keyword_refused(self):
"""Non-goal guard: a positional label mixed with a keyword is NOT the Arkane path form
(Arkane loads a path only when ``len(kwargs) == 0``) and is still refused."""
(call,) = [n.value for n in ast.parse("transitionState('TS2', path='qm/TS2.py')").body]
with pytest.raises(ValueError, match='POSITIONAL'):
_call_keywords(call, call_name='transitionState')
Loading