Skip to content
This repository was archived by the owner on May 5, 2026. It is now read-only.

Commit 7558b2f

Browse files
authored
Add files via upload
1 parent f9c58ae commit 7558b2f

1 file changed

Lines changed: 237 additions & 3 deletions

File tree

multioptpy/Utils/rcmc.py

Lines changed: 237 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
rcmc.py - Rate Constant Matrix Contraction (RCMC) Queue
33
=======================================================
4-
Queue implementation applying the RCMC algorithm (Type A) to dynamically
4+
Queue implementation applying the RCMC algorithm to dynamically
55
expanding chemical reaction networks. Determines the exploration priority
66
of frontier EQ nodes based on transient population dynamics.
77
@@ -14,7 +14,36 @@
1414

1515
import numpy as np
1616
from scipy.linalg import lu_factor, lu_solve
17-
from multioptpy.Wrapper.mapper import ExplorationQueue, ExplorationTask
17+
try:
18+
from multioptpy.Wrapper.mapper import ExplorationQueue, ExplorationTask
19+
except ImportError:
20+
# ── Minimal stub base classes used when running as __main__ ──────────
21+
# These are NOT part of RCMCQueue and are never used in normal library
22+
# operation (multioptpy must be installed for that).
23+
class ExplorationQueue: # type: ignore[no-redef]
24+
"""Stub base class – library mode requires the real multioptpy."""
25+
def __init__(self, rng_seed: int = 42) -> None:
26+
self._tasks: list = []
27+
28+
def push(self, task) -> None:
29+
self._tasks.append(task)
30+
31+
class ExplorationTask: # type: ignore[no-redef]
32+
"""Stub task object – mirrors the real ExplorationTask signature."""
33+
def __init__(
34+
self,
35+
node_id: int,
36+
xyz_file: str = "",
37+
afir_params: dict | None = None,
38+
priority: float = 0.0,
39+
metadata: dict | None = None,
40+
**kwargs,
41+
) -> None:
42+
self.node_id = node_id
43+
self.xyz_file = xyz_file
44+
self.afir_params = afir_params if afir_params is not None else {}
45+
self.priority = priority
46+
self.metadata = metadata if metadata is not None else {}
1847

1948
logger = logging.getLogger(__name__)
2049

@@ -410,4 +439,209 @@ def pop(self) -> ExplorationTask | None:
410439
if len(self._tasks) > 10:
411440
pop_lines.append(f" ... ({len(self._tasks) - 10} more)")
412441
logger.debug("RCMC remaining queue (top 10):\n%s", "\n".join(pop_lines))
413-
return selected
442+
return selected
443+
444+
# ══════════════════════════════════════════════════════════════════════════════
445+
# Standalone CLI entry point
446+
# Usage:
447+
# python rcmc.py reaction_network.json --temperature 500 --time 1e-6
448+
# python rcmc.py reaction_network.json -T 300 -t 1.0 --start-node 0 --output-dir ./out
449+
# ══════════════════════════════════════════════════════════════════════════════
450+
451+
if __name__ == "__main__":
452+
import argparse
453+
import json
454+
import sys
455+
from dataclasses import dataclass
456+
457+
# ── Argument parsing ──────────────────────────────────────────────────────
458+
parser = argparse.ArgumentParser(
459+
prog="rcmc.py",
460+
description=(
461+
"Standalone RCMC analysis.\n"
462+
"Loads a reaction-network JSON, builds the rate-constant matrix,\n"
463+
"applies Type-A contraction up to the specified time scale, and\n"
464+
"reports the transient population distribution for each EQ node."
465+
),
466+
formatter_class=argparse.RawDescriptionHelpFormatter,
467+
)
468+
parser.add_argument(
469+
"json_file",
470+
help="Path to reaction_network.json produced by MultiOptPy.",
471+
)
472+
parser.add_argument(
473+
"-T", "--temperature",
474+
type=float,
475+
default=300.0,
476+
metavar="K",
477+
help="Temperature in Kelvin (default: 300.0).",
478+
)
479+
parser.add_argument(
480+
"-t", "--time",
481+
type=float,
482+
default=1.0,
483+
metavar="S",
484+
help="Reaction time scale in seconds (default: 1.0).",
485+
)
486+
parser.add_argument(
487+
"--start-node",
488+
type=int,
489+
default=0,
490+
metavar="ID",
491+
help="node_id of the initial EQ node (default: 0).",
492+
)
493+
parser.add_argument(
494+
"--output-dir",
495+
type=str,
496+
default=None,
497+
metavar="DIR",
498+
help=(
499+
"Directory to write rcmc_K_contracted.csv "
500+
"(default: none – results printed to stdout only)."
501+
),
502+
)
503+
parser.add_argument(
504+
"--log-level",
505+
type=str,
506+
default="WARNING",
507+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
508+
help="Logging verbosity (default: WARNING).",
509+
)
510+
args = parser.parse_args()
511+
512+
# ── Logging setup ─────────────────────────────────────────────────────────
513+
logging.basicConfig(
514+
level=getattr(logging, args.log_level),
515+
format="%(levelname)s %(name)s: %(message)s",
516+
stream=sys.stderr,
517+
)
518+
519+
# ── Lightweight graph stubs ───────────────────────────────────────────────
520+
@dataclass
521+
class _Node:
522+
node_id: int
523+
energy: float # Hartree
524+
xyz_file: str = ""
525+
has_real_energy: bool = True
526+
527+
@dataclass
528+
class _Edge:
529+
node_id_1: int
530+
node_id_2: int
531+
ts_energy: float # Hartree (None-safe: kept as float here)
532+
533+
class _Graph:
534+
def __init__(self, nodes: list, edges: list) -> None:
535+
self._nodes = nodes
536+
self._edges = edges
537+
538+
def all_nodes(self) -> list:
539+
return self._nodes
540+
541+
def all_edges(self) -> list:
542+
return self._edges
543+
544+
# ── Load JSON ─────────────────────────────────────────────────────────────
545+
try:
546+
with open(args.json_file, encoding="utf-8") as fh:
547+
data = json.load(fh)
548+
except (OSError, json.JSONDecodeError) as exc:
549+
sys.exit(f"ERROR: Cannot read '{args.json_file}': {exc}")
550+
551+
raw_nodes = data.get("nodes", [])
552+
raw_edges = data.get("edges", [])
553+
554+
if not raw_nodes:
555+
sys.exit("ERROR: No nodes found in the JSON file.")
556+
557+
nodes = [
558+
_Node(
559+
node_id=nd["node_id"],
560+
energy=nd["energy_hartree"],
561+
xyz_file=nd.get("xyz_file", ""),
562+
)
563+
for nd in raw_nodes
564+
]
565+
566+
edges = []
567+
for ed in raw_edges:
568+
ts_e = ed.get("ts_energy_hartree")
569+
if ts_e is None:
570+
continue # skip edges without a TS energy
571+
if ed["node_id_1"] == ed["node_id_2"]:
572+
continue # skip self-loops
573+
edges.append(
574+
_Edge(
575+
node_id_1=ed["node_id_1"],
576+
node_id_2=ed["node_id_2"],
577+
ts_energy=ts_e,
578+
)
579+
)
580+
581+
graph = _Graph(nodes, edges)
582+
node_ids = {nd.node_id for nd in nodes}
583+
584+
# ── Build RCMCQueue and populate with stub tasks ──────────────────────────
585+
queue = RCMCQueue(
586+
temperature_K=args.temperature,
587+
reaction_time_s=args.time,
588+
start_node_id=args.start_node,
589+
output_dir=args.output_dir,
590+
)
591+
queue.set_graph(graph)
592+
593+
# Add every EQ node as a candidate task so pop() can rank them all.
594+
for nd in nodes:
595+
queue._tasks.append(
596+
ExplorationTask(
597+
node_id=nd.node_id,
598+
xyz_file=nd.xyz_file if hasattr(nd, "xyz_file") else "",
599+
afir_params={},
600+
priority=0.0,
601+
metadata={"delta_E_hartree": nd.energy},
602+
)
603+
)
604+
605+
# ── Run RCMC – one full pop() pass ────────────────────────────────────────
606+
print(
607+
f"\n{'═'*60}\n"
608+
f" RCMC standalone analysis\n"
609+
f" JSON : {args.json_file}\n"
610+
f" Nodes : {len(nodes)} | Edges: {len(edges)}\n"
611+
f" T : {args.temperature} K\n"
612+
f" Time scale: {args.time:.3e} s\n"
613+
f" Start node: EQ{args.start_node}\n"
614+
f"{'═'*60}"
615+
)
616+
617+
top_task = queue.pop()
618+
619+
# ── Print ranked population distribution ─────────────────────────────────
620+
# After pop() the remaining tasks carry updated priorities (populations).
621+
# Re-collect all tasks including the one that was popped.
622+
all_tasks = list(queue._tasks)
623+
if top_task is not None:
624+
all_tasks.insert(0, top_task) # re-insert at front (highest priority)
625+
626+
all_tasks.sort(key=lambda t: t.priority, reverse=True)
627+
628+
col_w = max(len(f"EQ{t.node_id}") for t in all_tasks) + 2
629+
print(f"\n{'Node':<{col_w}} {'Population':>18} {'Rank':>6}")
630+
print(f"{'─'*col_w} {'─'*18} {'─'*6}")
631+
for rank, task in enumerate(all_tasks, start=1):
632+
marker = " ◀ highest priority" if rank == 1 else ""
633+
print(
634+
f"EQ{task.node_id:<{col_w-2}} {task.priority:18.8e} {rank:6d}{marker}"
635+
)
636+
637+
if top_task is not None:
638+
print(
639+
f"\nRCMC recommends exploring: EQ{top_task.node_id} "
640+
f"(population = {top_task.priority:.6e})"
641+
)
642+
643+
if args.output_dir:
644+
out_file = os.path.join(args.output_dir, "rcmc_K_contracted.csv")
645+
print(f"\nContracted K-matrix + populations saved to: {out_file}")
646+
647+
print()

0 commit comments

Comments
 (0)