@@ -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