Skip to content

Commit bdac4b0

Browse files
authored
Add files via upload
1 parent 7832abe commit bdac4b0

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

multioptpy/Wrapper/mapper.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)