Skip to content

Commit d85e026

Browse files
authored
Add files via upload
1 parent 9eb71c2 commit d85e026

1 file changed

Lines changed: 103 additions & 14 deletions

File tree

multioptpy/Utils/rcmc.py

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def __init__(
6060
rng_seed: int = 42,
6161
start_node_id: int = 0,
6262
output_dir: str | None = None,
63+
use_free_energy: bool = False,
6364
) -> None:
6465
super().__init__(rng_seed=rng_seed)
6566
self.temperature_K = temperature_K
@@ -68,6 +69,10 @@ def __init__(
6869
self.output_dir = output_dir
6970
self.graph = None
7071
self._pop_count: int = 0
72+
# When True, use Gibbs free energy (G_tot) in place of electronic
73+
# energy for rate-constant and population calculations.
74+
# Defaults to False for backward compatibility.
75+
self.use_free_energy: bool = use_free_energy
7176

7277
def compute_priority(self, task: ExplorationTask) -> float:
7378
"""
@@ -216,7 +221,26 @@ def pop(self) -> ExplorationTask | None:
216221
self._tasks.sort(key=lambda t: t.metadata.get("delta_E_hartree", 0.0))
217222
return self._tasks.pop(0)
218223

219-
nodes = [n for n in self.graph.all_nodes() if n.has_real_energy]
224+
# ── Select nodes and energy accessor based on energy mode ───────────
225+
# In free-energy mode only nodes that carry a G_tot value are included;
226+
# nodes with free_energy=None are silently excluded (they were already
227+
# filtered/warned at the CLI level before pop() is called).
228+
if self.use_free_energy:
229+
nodes = [
230+
n for n in self.graph.all_nodes()
231+
if getattr(n, "free_energy", None) is not None
232+
]
233+
def _node_e(n):
234+
return n.free_energy
235+
def _edge_ts_e(edge):
236+
return getattr(edge, "ts_free_energy", None)
237+
else:
238+
nodes = [n for n in self.graph.all_nodes() if n.has_real_energy]
239+
def _node_e(n):
240+
return n.energy
241+
def _edge_ts_e(edge):
242+
return edge.ts_energy
243+
220244
if not nodes:
221245
return self._tasks.pop(0)
222246

@@ -228,16 +252,17 @@ def pop(self) -> ExplorationTask | None:
228252
RT_ha = K_B_HARTREE * self.temperature_K
229253

230254
for edge in self.graph.all_edges():
231-
if edge.ts_energy is None:
255+
ts_e = _edge_ts_e(edge)
256+
if ts_e is None:
232257
continue
233258
if edge.node_id_1 not in node_to_idx or edge.node_id_2 not in node_to_idx:
234259
continue
235-
260+
236261
u = node_to_idx[edge.node_id_1]
237262
v = node_to_idx[edge.node_id_2]
238-
E_u = nodes[u].energy
239-
E_v = nodes[v].energy
240-
E_TS = edge.ts_energy
263+
E_u = _node_e(nodes[u])
264+
E_v = _node_e(nodes[v])
265+
E_TS = ts_e
241266

242267
E_TS_u = max(E_TS, E_u)
243268
E_TS_v = max(E_TS, E_v)
@@ -265,7 +290,7 @@ def pop(self) -> ExplorationTask | None:
265290
if self.start_node_id in node_to_idx:
266291
p[node_to_idx[self.start_node_id]] = 1.0
267292
else:
268-
start_idx = np.argmin([n.energy for n in nodes])
293+
start_idx = np.argmin([_node_e(n) for n in nodes])
269294
p[start_idx] = 1.0
270295

271296
S = []
@@ -510,6 +535,20 @@ def main(argv: list[str] | None = None) -> None:
510535
"(default: none – results printed to stdout only)."
511536
),
512537
)
538+
parser.add_argument(
539+
"--energy-type",
540+
type=str,
541+
default="electronic",
542+
choices=["electronic", "free"],
543+
metavar="TYPE",
544+
help=(
545+
"Energy type used for rate-constant calculations. "
546+
" 'electronic' (default) uses the SCF energy (energy_hartree). "
547+
" 'free' uses the Gibbs free energy (free_energy_hartree). "
548+
" In free mode, nodes whose free_energy_hartree is null are "
549+
" removed along with all edges connected to them before RCMC."
550+
),
551+
)
513552
parser.add_argument(
514553
"--log-level",
515554
type=str,
@@ -530,15 +569,17 @@ def main(argv: list[str] | None = None) -> None:
530569
@dataclass
531570
class _Node:
532571
node_id: int
533-
energy: float # Hartree
572+
energy: float | None # Hartree (electronic)
534573
xyz_file: str = ""
535574
has_real_energy: bool = True
575+
free_energy: float | None = None # Hartree (Gibbs, None if unavailable)
536576

537577
@dataclass
538578
class _Edge:
539579
node_id_1: int
540580
node_id_2: int
541-
ts_energy: float # Hartree (None-safe: kept as float here)
581+
ts_energy: float | None # Hartree (electronic TS energy)
582+
ts_free_energy: float | None = None # Hartree (free-energy TS)
542583

543584
class _Graph:
544585
def __init__(self, nodes: list, edges: list) -> None:
@@ -564,27 +605,71 @@ def all_edges(self) -> list:
564605
if not raw_nodes:
565606
sys.exit("ERROR: No nodes found in the JSON file.")
566607

608+
use_free_energy: bool = (args.energy_type == "free")
609+
567610
nodes = [
568611
_Node(
569612
node_id=nd["node_id"],
570613
energy=nd["energy_hartree"],
571614
xyz_file=nd.get("xyz_file", ""),
615+
has_real_energy=nd["energy_hartree"] is not None,
616+
free_energy=nd.get("free_energy_hartree"),
572617
)
573618
for nd in raw_nodes
574619
]
575620

621+
# ── Free-energy mode: remove nodes with null G_tot and their edges ────
622+
if use_free_energy:
623+
null_ids = {nd.node_id for nd in nodes if nd.free_energy is None}
624+
if null_ids:
625+
print(
626+
f"WARNING: The following nodes have no free energy and will be "
627+
f"excluded from the RCMC calculation:\n"
628+
+ "\n".join(f" EQ{nid}" for nid in sorted(null_ids))
629+
)
630+
nodes = [nd for nd in nodes if nd.node_id not in null_ids]
631+
if not nodes:
632+
sys.exit(
633+
"ERROR: No nodes with free_energy_hartree remain after filtering. "
634+
"Run without --energy-type free or recompute with vibrational analysis."
635+
)
636+
# Check start node before filtering edges
637+
if args.start_node in null_ids:
638+
sys.exit(
639+
f"ERROR: --start-node EQ{args.start_node} has no free energy "
640+
f"and was excluded. Choose a different start node or use "
641+
f"--energy-type electronic."
642+
)
643+
576644
edges = []
577645
for ed in raw_edges:
578-
ts_e = ed.get("ts_energy_hartree")
579-
if ts_e is None:
580-
continue # skip edges without a TS energy
646+
# In free-energy mode use ts_free_energy; fall back to ts_energy otherwise.
647+
if use_free_energy:
648+
ts_e = None # not used for K-matrix in free mode
649+
ts_free_e = ed.get("ts_free_energy_hartree")
650+
if ts_free_e is None:
651+
continue # skip edges without a free TS energy
652+
else:
653+
ts_e = ed.get("ts_energy_hartree")
654+
ts_free_e = ed.get("ts_free_energy_hartree")
655+
if ts_e is None:
656+
continue # skip edges without a TS energy
657+
581658
if ed["node_id_1"] == ed["node_id_2"]:
582-
continue # skip self-loops
659+
continue # skip self-loops
660+
661+
# In free-energy mode skip edges connected to excluded (null G) nodes
662+
if use_free_energy and (
663+
ed["node_id_1"] in null_ids or ed["node_id_2"] in null_ids
664+
):
665+
continue
666+
583667
edges.append(
584668
_Edge(
585669
node_id_1=ed["node_id_1"],
586670
node_id_2=ed["node_id_2"],
587671
ts_energy=ts_e,
672+
ts_free_energy=ts_free_e,
588673
)
589674
)
590675

@@ -597,22 +682,25 @@ def all_edges(self) -> list:
597682
reaction_time_s=args.time,
598683
start_node_id=args.start_node,
599684
output_dir=args.output_dir,
685+
use_free_energy=use_free_energy,
600686
)
601687
queue.set_graph(graph)
602688

603689
# Add every EQ node as a candidate task so pop() can rank them all.
604690
for nd in nodes:
691+
eff_e = nd.free_energy if (use_free_energy and nd.free_energy is not None) else nd.energy
605692
queue._tasks.append(
606693
ExplorationTask(
607694
node_id=nd.node_id,
608695
xyz_file=nd.xyz_file if hasattr(nd, "xyz_file") else "",
609696
afir_params={},
610697
priority=0.0,
611-
metadata={"delta_E_hartree": nd.energy},
698+
metadata={"delta_E_hartree": eff_e},
612699
)
613700
)
614701

615702
# ── Run RCMC – one full pop() pass ────────────────────────────────────────
703+
energy_label = "Gibbs free energy (G_tot)" if use_free_energy else "Electronic energy (SCF)"
616704
print(
617705
f"\n{'═'*60}\n"
618706
f" RCMC standalone analysis\n"
@@ -621,6 +709,7 @@ def all_edges(self) -> list:
621709
f" T : {args.temperature} K\n"
622710
f" Time scale: {args.time:.3e} s\n"
623711
f" Start node: EQ{args.start_node}\n"
712+
f" Energy : {energy_label}\n"
624713
f"{'═'*60}"
625714
)
626715

0 commit comments

Comments
 (0)