Skip to content

Commit 97ff75b

Browse files
committed
Updates
1 parent 272fc55 commit 97ff75b

5 files changed

Lines changed: 128 additions & 12 deletions

File tree

arc/main.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from arc.job.ssh import SSHClient
3737
from arc.output import write_output_yml
3838
from arc.processor import process_arc_project, resolve_neb_level
39+
from arc.provenance import DecisionKind, EdgeType
3940
from arc.reaction import ARCReaction
4041
from arc.scheduler import Scheduler
4142
from arc.species.converter import str_to_xyz
@@ -671,6 +672,70 @@ def execute(self) -> dict:
671672
log_footer(execution_time=self.execution_time)
672673
return status_dict
673674

675+
def _add_arkane_provenance_nodes(self):
676+
"""Add Arkane computation and result nodes to the provenance graph.
677+
678+
For each converged species with thermo results, creates:
679+
convergence_confirmed → calc(statmech_thermo) → data(thermo)
680+
681+
For each converged reaction with kinetics results, creates:
682+
convergence_confirmed → calc(statmech_kinetics) → data(kinetics)
683+
"""
684+
graph = self.scheduler.graph
685+
for spc in self.scheduler.species_dict.values():
686+
if spc.is_ts or getattr(spc.thermo, 'H298', None) is None:
687+
continue
688+
spc_nid = graph.find_species_node(spc.label)
689+
if spc_nid is None:
690+
continue
691+
# Insert a CalculationNode for the Arkane thermo computation.
692+
calc_nid = graph.add_calculation_node(
693+
label=spc.label,
694+
job_name='arkane_thermo',
695+
job_type='statmech_thermo',
696+
job_adapter=self.thermo_adapter,
697+
status='done',
698+
)
699+
graph.add_edge(spc_nid, calc_nid, EdgeType.belongs_to)
700+
# Link from convergence gate if it exists.
701+
conv_nodes = graph.query(decision_kind=DecisionKind.convergence_confirmed, label=spc.label)
702+
for conv_node in conv_nodes:
703+
graph.add_edge(conv_node.node_id, calc_nid, EdgeType.triggered_by)
704+
thermo_nid = graph.add_data_node(
705+
label=spc.label,
706+
data_kind='thermo',
707+
value=f'H298={spc.thermo.H298:.1f} kJ/mol, S298={spc.thermo.S298:.1f} J/mol/K',
708+
)
709+
graph.add_edge(calc_nid, thermo_nid, EdgeType.output_of)
710+
for rxn in self.scheduler.rxn_list:
711+
if rxn.kinetics is None:
712+
continue
713+
ts_nid = graph.find_species_node(rxn.ts_label)
714+
if ts_nid is None:
715+
continue
716+
# Insert a CalculationNode for the Arkane kinetics computation.
717+
calc_nid = graph.add_calculation_node(
718+
label=rxn.ts_label,
719+
job_name='arkane_kinetics',
720+
job_type='statmech_kinetics',
721+
job_adapter=self.kinetics_adapter,
722+
status='done',
723+
)
724+
graph.add_edge(ts_nid, calc_nid, EdgeType.belongs_to)
725+
# Link from TS convergence gate if it exists.
726+
conv_nodes = graph.query(decision_kind=DecisionKind.convergence_confirmed, label=rxn.ts_label)
727+
for conv_node in conv_nodes:
728+
graph.add_edge(conv_node.node_id, calc_nid, EdgeType.triggered_by)
729+
ea = rxn.kinetics.get('Ea')
730+
ea_str = f', Ea={ea[0]:.1f} {ea[1]}' if ea else ''
731+
kinetics_nid = graph.add_data_node(
732+
label=rxn.ts_label,
733+
data_kind='kinetics',
734+
value=f'{rxn.label}{ea_str}',
735+
)
736+
graph.add_edge(calc_nid, kinetics_nid, EdgeType.output_of)
737+
graph.save(self.scheduler.graph_path)
738+
674739
def save_project_info_file(self):
675740
"""
676741
Save a project info file.

arc/plotter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ def render_provenance_graph(prov_graph, run_label: str = 'ARC run') -> 'graphviz
120120
'ts_validation_freq': 'lightyellow',
121121
'ts_validation_nmd': 'lightyellow',
122122
'ts_validation_irc': 'lightyellow',
123+
'ts_validation_e0': 'lightyellow',
124+
'ts_validation_e_elect': 'lightyellow',
123125
'ts_switch': 'mistyrose',
126+
'convergence_confirmed': 'palegreen',
124127
}
125128

126129
# Edge styling lookup

arc/provenance/nodes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ class DecisionKind(str, Enum):
5555
ts_validation_freq = 'ts_validation_freq'
5656
ts_validation_nmd = 'ts_validation_nmd'
5757
ts_validation_irc = 'ts_validation_irc'
58+
ts_validation_e0 = 'ts_validation_e0'
59+
ts_validation_e_elect = 'ts_validation_e_elect'
5860
ts_switch = 'ts_switch'
5961
job_troubleshooting = 'job_troubleshooting'
6062
ts_method_spawning = 'ts_method_spawning'
63+
convergence_confirmed = 'convergence_confirmed'
6164

6265

6366
class EdgeType(str, Enum):

arc/provenance/provenance_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ def test_data_kind_values(self):
3333
def test_decision_kind_values(self):
3434
expected = {'conformer_selection', 'ts_guess_clustering', 'ts_guess_selection',
3535
'ts_guess_selection_failed', 'ts_validation_freq', 'ts_validation_nmd',
36-
'ts_validation_irc', 'ts_switch', 'job_troubleshooting', 'ts_method_spawning'}
36+
'ts_validation_irc', 'ts_validation_e0', 'ts_validation_e_elect',
37+
'ts_switch', 'job_troubleshooting', 'ts_method_spawning',
38+
'convergence_confirmed'}
3739
actual = {dk.value for dk in DecisionKind}
3840
self.assertEqual(expected, actual)
3941

arc/scheduler.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2888,12 +2888,12 @@ def check_freq_job(self,
28882888
f'Status is:\n{self.species_dict[label].ts_checks}\n'
28892889
f'Searching for a better TS conformer...')
28902890
# ── Graph: record NMD validation failure ──
2891-
self.graph.add_decision_node(
2891+
nmd_fail_nid = self.graph.add_decision_node(
28922892
label=label,
28932893
decision_kind=DecisionKind.ts_validation_nmd,
28942894
outcome='Failed: normal mode displacement check',
28952895
)
2896-
self.switch_ts(label)
2896+
self.switch_ts(label, triggered_by_nid=nmd_fail_nid)
28972897
switch_ts = True
28982898
if wrong_freq_message in self.output[label]['warnings']:
28992899
self.output[label]['warnings'] = ''.join(self.output[label]['warnings'].split(wrong_freq_message))
@@ -2961,14 +2961,14 @@ def check_negative_freq(self,
29612961
f'Status is:\n{self.species_dict[label].ts_checks}\n'
29622962
f'Searching for a better TS conformer...')
29632963
# ── Graph: record TS freq validation failure ──
2964-
self.graph.add_decision_node(
2964+
freq_fail_nid = self.graph.add_decision_node(
29652965
label=label,
29662966
decision_kind=DecisionKind.ts_validation_freq,
29672967
criteria={'neg_freqs': [float(f) for f in neg_freqs],
29682968
'expected': 1},
29692969
outcome=f'Failed: {len(neg_freqs)} imaginary freqs, switching TS',
29702970
)
2971-
self.switch_ts(label=label)
2971+
self.switch_ts(label=label, triggered_by_nid=freq_fail_nid)
29722972
return False
29732973
else:
29742974
logger.info(f'TS {label} has exactly one imaginary frequency: {neg_freqs[0]}')
@@ -3030,7 +3030,13 @@ def check_rxn_e0_by_spc(self, label: str):
30303030
if rxn.ts_species.ts_checks['E0'] is False:
30313031
logger.info(f'TS {rxn.ts_species.label} of reaction {rxn.label} did not pass the E0 check.\n'
30323032
f'Searching for a better TS conformer...\n')
3033-
self.switch_ts(rxn.ts_label)
3033+
# ── Graph: record E0 validation failure ──
3034+
e0_fail_nid = self.graph.add_decision_node(
3035+
label=rxn.ts_label,
3036+
decision_kind=DecisionKind.ts_validation_e0,
3037+
outcome=f'Failed: TS E0 not above both wells for {rxn.label}',
3038+
)
3039+
self.switch_ts(rxn.ts_label, triggered_by_nid=e0_fail_nid)
30343040
if self.species_dict[rxn.ts_label].ts_guesses_exhausted \
30353041
or self.species_dict[rxn.ts_label].chosen_ts is None:
30363042
logger.warning(f'Could not find a valid TS conformer for {rxn.ts_label} '
@@ -3039,27 +3045,50 @@ def check_rxn_e0_by_spc(self, label: str):
30393045
# Restore E0 failure flag — switch_ts resets ts_checks via populate_ts_checks().
30403046
# check_all_done reads this to avoid overwriting convergence back to True.
30413047
self.species_dict[rxn.ts_label].ts_checks['E0'] = False
3048+
elif rxn.ts_species.ts_checks['E0'] is True:
3049+
# ── Graph: record E0 validation pass ──
3050+
self.graph.add_decision_node(
3051+
label=rxn.ts_label,
3052+
decision_kind=DecisionKind.ts_validation_e0,
3053+
outcome=f'Passed: TS E0 above both wells for {rxn.label}',
3054+
)
3055+
# Also record e_elect check if it ran as a fallback.
3056+
if rxn.ts_species.ts_checks['e_elect'] is True:
3057+
self.graph.add_decision_node(
3058+
label=rxn.ts_label,
3059+
decision_kind=DecisionKind.ts_validation_e_elect,
3060+
outcome=f'Passed: TS e_elect above both wells for {rxn.label}',
3061+
)
3062+
elif rxn.ts_species.ts_checks['e_elect'] is False:
3063+
self.graph.add_decision_node(
3064+
label=rxn.ts_label,
3065+
decision_kind=DecisionKind.ts_validation_e_elect,
3066+
outcome=f'Warning: TS e_elect not above both wells for {rxn.label}',
3067+
)
30423068

3043-
def switch_ts(self, label: str):
3069+
def switch_ts(self, label: str, triggered_by_nid: Optional[str] = None):
30443070
"""
30453071
Try the next optimized TS guess in line if a previous TS guess was found to be wrong.
30463072
30473073
Args:
30483074
label (str): The TS species label.
3075+
triggered_by_nid (str, optional): Node ID of the validation decision that triggered this switch.
30493076
"""
30503077
logger.info(f'Switching a TS guess for {label}...')
30513078
old_chosen = self.species_dict[label].chosen_ts
30523079
self.determine_most_likely_ts_conformer(label=label) # Look for a different TS guess.
30533080
new_chosen = self.species_dict[label].chosen_ts
30543081
# ── Graph: record TS switch decision ──
3055-
self.graph.add_decision_node(
3082+
switch_nid = self.graph.add_decision_node(
30563083
label=label,
30573084
decision_kind=DecisionKind.ts_switch,
30583085
criteria={'old_chosen': old_chosen, 'new_chosen': new_chosen,
30593086
'exhausted': self.species_dict[label].ts_guesses_exhausted},
30603087
outcome=f'Switched from TSG #{old_chosen} to #{new_chosen}'
30613088
if new_chosen is not None else 'All TS guesses exhausted',
30623089
)
3090+
if triggered_by_nid is not None:
3091+
self.graph.add_edge(triggered_by_nid, switch_nid, EdgeType.triggered_by)
30633092
self.delete_all_species_jobs(label=label) # Delete other currently running jobs for this TS.
30643093
freq_path = os.path.join(self.project_directory, 'output', 'rxns', label, 'geometry', 'freq.out')
30653094
if os.path.isfile(freq_path):
@@ -3239,17 +3268,25 @@ def check_irc_species(self, label: str):
32393268
if len(self.output[ts_label]['paths']['irc']) == 2:
32403269
irc_species_labels = self.species_dict[ts_label].irc_label.split()
32413270
if all(self.output[irc_label]['paths']['geo'] for irc_label in irc_species_labels):
3271+
rxn = self.rxn_dict.get(self.species_dict[ts_label].rxn_index, None)
32423272
check_irc_species_and_rxn(
32433273
xyz_1=self.output[irc_species_labels[0]]['paths']['geo'],
32443274
xyz_2=self.output[irc_species_labels[1]]['paths']['geo'],
3245-
rxn=self.rxn_dict.get(self.species_dict[ts_label].rxn_index, None),
3275+
rxn=rxn,
32463276
)
3247-
# ── Graph: record IRC validation decision ──
3277+
# ── Graph: record IRC validation decision with actual outcome ──
3278+
irc_result = rxn.ts_species.ts_checks['IRC'] if rxn is not None else None
3279+
if irc_result is True:
3280+
irc_outcome = 'Passed: IRC endpoints match expected R/P'
3281+
elif irc_result is False:
3282+
irc_outcome = 'Failed: IRC endpoints do not match expected R/P'
3283+
else:
3284+
irc_outcome = 'Inconclusive: could not determine IRC match'
32483285
self.graph.add_decision_node(
32493286
label=ts_label,
32503287
decision_kind=DecisionKind.ts_validation_irc,
3251-
criteria={'irc_species': irc_species_labels},
3252-
outcome='IRC validation completed',
3288+
criteria={'irc_species': irc_species_labels, 'result': irc_result},
3289+
outcome=irc_outcome,
32533290
)
32543291

32553292
def check_scan_job(self,
@@ -3513,6 +3550,12 @@ def check_all_done(self, label: str):
35133550
all_converged = False
35143551
if label in self.output and all_converged:
35153552
self.output[label]['convergence'] = True
3553+
# ── Graph: record convergence gate ──
3554+
self.graph.add_decision_node(
3555+
label=label,
3556+
decision_kind=DecisionKind.convergence_confirmed,
3557+
outcome=f'All required calculations converged',
3558+
)
35163559
if self.species_dict[label].is_ts:
35173560
self.species_dict[label].make_ts_report()
35183561
logger.info(self.species_dict[label].ts_report + '\n')

0 commit comments

Comments
 (0)