@@ -1480,6 +1480,11 @@ def __init__(
14801480 self .graph = NetworkGraph ()
14811481 self ._iteration : int = 0
14821482
1483+ # Accumulates energy backfill requests discovered during each iteration.
1484+ # Maps node_id -> {"energy": float|None, "free_energy": float|None}.
1485+ # Applied in bulk by _flush_node_energy_updates() before graph.save().
1486+ self ._pending_node_updates : dict [int , dict ] = {}
1487+
14831488 os .makedirs (self .output_dir , exist_ok = True )
14841489 os .makedirs (self .work_dir , exist_ok = True )
14851490 os .makedirs (os .path .join (self .output_dir , "nodes" ), exist_ok = True )
@@ -1638,6 +1643,7 @@ def run(self) -> None:
16381643 profile_dirs = self ._run_autots (task , run_dir )
16391644 except Exception as e :
16401645 logger .error (f"AutoTS failed for run { run_dir } : { e } " )
1646+ self ._flush_node_energy_updates ()
16411647 self ._save_run_metadata (run_dir , task , status = "FAILED" , profile_dirs = [])
16421648 self .graph .save (self .graph_json_path )
16431649 self ._write_priority_log (priority_log )
@@ -1656,6 +1662,7 @@ def run(self) -> None:
16561662 if hasattr (self .queue , "set_graph" ):
16571663 self .queue .set_graph (self .graph )
16581664
1665+ self ._flush_node_energy_updates ()
16591666 self ._save_run_metadata (run_dir , task , status = "DONE" , profile_dirs = profile_dirs )
16601667 self .graph .save (self .graph_json_path )
16611668 # Write after new nodes and tasks have been registered so the log
@@ -2205,6 +2212,62 @@ def _process_profile(self, profile_dir: str, run_dir: str) -> None:
22052212 f"{ result ['barrier_fwd' ]:.2f} " if result ["barrier_fwd" ] is not None else "N/A" ,
22062213 )
22072214
2215+ def _flush_node_energy_updates (self ) -> None :
2216+ """Apply pending energy backfill updates accumulated during the iteration.
2217+
2218+ When :meth:`_find_or_register_node` matches an incoming structure to an
2219+ existing :class:`EQNode` but that node was registered without energy or
2220+ free-energy data (e.g. the seed structure optimisation did not perform
2221+ thermochemistry), the new non-null values are queued in
2222+ ``_pending_node_updates``. This method applies all queued updates in
2223+ one batch, then clears the queue.
2224+
2225+ Design decisions (confirmed with user):
2226+ * Called once per iteration, just before ``graph.save()``.
2227+ * Both ``energy`` (electronic) and ``free_energy`` are updated when
2228+ the existing value is ``None`` and the incoming value is not.
2229+ * Existing TSEdge barrier values are **never** modified here — they
2230+ always reflect the actual measured values from the IRC run that
2231+ produced them (see Q3 design decision).
2232+ """
2233+ if not self ._pending_node_updates :
2234+ return
2235+
2236+ for node_id , updates in self ._pending_node_updates .items ():
2237+ node = self .graph .get_node (node_id )
2238+ if node is None :
2239+ logger .warning (
2240+ "_flush_node_energy_updates: node EQ%d not found in graph; skipping." ,
2241+ node_id ,
2242+ )
2243+ continue
2244+
2245+ changed : list [str ] = []
2246+
2247+ new_e = updates .get ("energy" )
2248+ if new_e is not None and node .energy is None :
2249+ node .energy = new_e
2250+ changed .append (f"energy={ new_e :.10f} Ha" )
2251+
2252+ new_g = updates .get ("free_energy" )
2253+ if new_g is not None and node .free_energy is None :
2254+ node .free_energy = new_g
2255+ changed .append (f"free_energy={ new_g :.10f} Ha" )
2256+
2257+ if changed :
2258+ logger .info (
2259+ "_flush_node_energy_updates: EQ%d updated — %s" ,
2260+ node_id , " " .join (changed ),
2261+ )
2262+ else :
2263+ logger .debug (
2264+ "_flush_node_energy_updates: EQ%d queued but no null fields "
2265+ "to fill (already populated by a prior update)." ,
2266+ node_id ,
2267+ )
2268+
2269+ self ._pending_node_updates .clear ()
2270+
22082271 def _find_or_register_node (
22092272 self ,
22102273 xyz_file : str ,
@@ -2291,6 +2354,29 @@ def _find_or_register_node(
22912354 "_find_or_register_node: MATCH EQ%d RMSD=%.4f A < threshold=%.3f A." ,
22922355 existing .node_id , rmsd , self .checker .rmsd_threshold ,
22932356 )
2357+ # ── Energy backfill ───────────────────────────────────────
2358+ # When the matched node was registered without energy or free-
2359+ # energy data (e.g. seed node from a plain optimisation) and
2360+ # the new incoming structure carries those values, queue an
2361+ # update so that _flush_node_energy_updates() can populate the
2362+ # null fields before the next graph.save().
2363+ needs_update : dict = {}
2364+ if existing .energy is None and energy is not None :
2365+ needs_update ["energy" ] = energy
2366+ if existing .free_energy is None and free_energy is not None :
2367+ needs_update ["free_energy" ] = free_energy
2368+ if needs_update :
2369+ prev = self ._pending_node_updates .get (existing .node_id , {})
2370+ # Only overwrite fields that are still absent in prior queued updates.
2371+ if "energy" not in prev and "energy" in needs_update :
2372+ prev ["energy" ] = needs_update ["energy" ]
2373+ if "free_energy" not in prev and "free_energy" in needs_update :
2374+ prev ["free_energy" ] = needs_update ["free_energy" ]
2375+ self ._pending_node_updates [existing .node_id ] = prev
2376+ logger .debug (
2377+ "_find_or_register_node: queued backfill for EQ%d: %s" ,
2378+ existing .node_id , needs_update ,
2379+ )
22942380 return existing .node_id
22952381 else :
22962382 logger .info (
0 commit comments