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