Skip to content

Commit e495225

Browse files
authored
Add files via upload
1 parent cec3638 commit e495225

1 file changed

Lines changed: 66 additions & 29 deletions

File tree

multioptpy/Utils/rcmc.py

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -509,30 +509,66 @@ def _edge_ts_e(edge):
509509
p_T = p[T]
510510

511511
try:
512-
# ── Build initial population for T super-states ───────────
513-
# Each T super-state collects the probability of every node
514-
# absorbed into it during RCMC contraction (the T representative
515-
# itself plus any S nodes whose fast dynamics were projected onto
516-
# it via Schur-complement elimination).
517-
# Summing p[members] is the standard probability-conserving
518-
# lumping step and is self-consistent with the quasi-steady-state
519-
# assumption used to derive D: within each super-state the fast
520-
# internal modes are already eliminated, so aggregating the raw
521-
# probability gives the correct effective initial condition for
522-
# the contracted dynamics.
523-
p_T_init = np.zeros(len(T), dtype=np.float64)
524-
for i, t_global in enumerate(T):
525-
members = superstate_members.get(t_global, [t_global])
526-
p_T_init[i] = float(np.sum(p[members]))
527-
528-
# Normalise: guards against floating-point drift and the rare
529-
# edge case where the start node ended up in S rather than T.
530-
total_p_T_init = p_T_init.sum()
531-
if total_p_T_init > 0.0:
532-
p_T_init /= total_p_T_init
533-
534-
# ── Factorise K_SS once; reused by both RK4 and QSS paths ─
512+
# ── Factorise K_SS once; reused throughout ────────────────
535513
lu, piv = lu_factor(K_SS_buf)
514+
515+
# ── Build initial population for T super-states ───────────
516+
# Correct approach depends on whether the start node ended up
517+
# in S or T after RCMC contraction:
518+
#
519+
# Case A — start_node ∈ T:
520+
# The initial condition is a delta function on the T-local
521+
# index of the start node. No probability should be spread
522+
# to other T super-states via superstate_members, because the
523+
# S-mode QSS equilibration acts on a timescale faster than the
524+
# current RCMC epoch; the start node itself has not yet reached
525+
# that fast equilibrium and should remain at p=1.
526+
#
527+
# Case B — start_node ∈ S:
528+
# The start node was eliminated as a fast mode. Its initial
529+
# probability (p=1) must be projected onto the T super-states
530+
# via the QSS back-projection:
531+
# p_T_init ≈ −K_TS · K_SS⁻¹ · e_{start}
532+
# which is the same adiabatic-elimination step used later to
533+
# recover q_S from q_T, applied here in reverse to the initial
534+
# condition.
535+
# (q_T = (I − Π_T⁻¹ K_TS K_SS⁻¹ π_S)⁻¹ (p_T − K_TS K_SS⁻¹ p_S)
536+
# for the case p_T = 0, p_S = e_start, without the Boltzmann
537+
# correction Π since π is not available here.)
538+
#
539+
# The previous implementation used superstate_members to lump
540+
# p[members] into each T super-state. This is incorrect when
541+
# the start node is absorbed (via Schur coupling) into a product-
542+
# side T super-state: it assigns all initial probability to the
543+
# wrong cluster, producing grossly incorrect populations.
544+
start_global_idx = node_to_idx.get(self.start_node_id)
545+
if start_global_idx in T:
546+
# Case A: delta function at start node's T-local position
547+
p_T_init = np.zeros(len(T), dtype=np.float64)
548+
p_T_init[T.index(start_global_idx)] = 1.0
549+
else:
550+
# Case B: QSS back-projection from S
551+
start_S_pos = S.index(start_global_idx) if start_global_idx in S else None
552+
if start_S_pos is not None:
553+
e_start = np.zeros(len(S), dtype=np.float64)
554+
e_start[start_S_pos] = 1.0
555+
x_start = lu_solve((lu, piv), e_start) # K_SS⁻¹ · e_start
556+
p_T_init = -(K_TS @ x_start)
557+
p_T_init = np.maximum(p_T_init, 0.0)
558+
total = p_T_init.sum()
559+
if total > 0.0:
560+
p_T_init /= total
561+
else:
562+
# Fallback: uniform over T (should not normally occur)
563+
logger.warning(
564+
"RCMC p_T_init back-projection gave all-zero vector "
565+
"(start_node=EQ%d in S); using uniform T distribution.",
566+
self.start_node_id,
567+
)
568+
p_T_init = np.ones(len(T), dtype=np.float64) / len(T)
569+
else:
570+
# start_node not found in either S or T (disconnected?)
571+
p_T_init = np.ones(len(T), dtype=np.float64) / len(T)
536572
X_ST = lu_solve((lu, piv), K_ST)
537573

538574
# ── Primary path: integrate contracted dynamics via RK4 ───
@@ -556,14 +592,15 @@ def _edge_ts_e(edge):
556592
"(|T|=%d, |S|=%d, t=%.3e s).",
557593
self._pop_count, len(T), len(S), self.reaction_time_s,
558594
)
559-
X_pS = lu_solve((lu, piv), p_S)
560-
X_ST_2 = lu_solve((lu, piv), X_ST)
561-
M = np.eye(len(T)) + K_TS @ X_ST_2
562-
m_vec = np.sum(M, axis=0)
595+
# Type-A reference (without Boltzmann π):
596+
# q_T = diag(1 - K_TS K_SS⁻¹ 1_S)⁻¹ · (p_T - K_TS K_SS⁻¹ p_S)
597+
X_pS = lu_solve((lu, piv), p_S) # K_SS⁻¹ p_S
598+
x_ones = lu_solve((lu, piv), np.ones(len(S))) # K_SS⁻¹ 1_S
599+
v = 1.0 - K_TS @ x_ones # 1 - K_TS K_SS⁻¹ 1_S (|T|,)
563600
V_TT_diag = 1.0 / np.where(
564-
np.abs(m_vec) > 1e-16, m_vec, 1e-16
601+
np.abs(v) > 1e-16, v, 1e-16
565602
)
566-
q_T = V_TT_diag * (p_T - K_TS @ X_pS)
603+
q_T = V_TT_diag * (p_T_init - K_TS @ X_pS)
567604

568605
# ── Back-project S populations via quasi-steady-state ─────
569606
# Under the adiabatic-elimination assumption that underpins D:

0 commit comments

Comments
 (0)