11"""Mpemba layer: D19 slowest-mode overlap, D20 expansion-coefficient scaling.
22
33D19 -- :func:`overlap_c1`. The Mpemba-overlap test (Carollo 2021). Decompose
4- the initial state in left-eigenvector basis of L and test whether the
4+ the initial state in the left-eigenvector basis of L and test whether the
55coefficient on the slowest mode (lambda_1 != 0) vanishes. If ``|c_1| << eps``,
6- the system relaxes anomalously fast (Mpemba candidate).
6+ the state relaxes anomalously fast (strong-Mpemba *candidate*).
7+
8+ The coefficient reported is the **biorthogonal** expansion coefficient
9+ ``c_1 = <l_1, rho_0> / <l_1, r_1>`` -- the physically meaningful weight of the
10+ slowest right-mode ``r_1`` in the decomposition of ``rho_0`` (l_1, r_1 the left
11+ and right eigenvectors of the slowest non-zero mode). Normalising by
12+ ``<l_1, r_1>`` rather than ``||l_1||`` (the previous behaviour, for which only
13+ the zero test was meaningful) makes ``c_1`` the actual expansion magnitude.
14+
15+ **Non-triviality guard (LIOU-CLS-002, issue #68).** A vanishing ``c_1`` is only
16+ an *anomalous* Mpemba skip when a state that could generically excite the
17+ slowest mode is fine-tuned so it does not. A ``rho_0`` that **commutes with the
18+ steady state** ``rho_ss`` lies in the classical (population) sector of
19+ ``rho_ss``; if the slowest mode is a *coherence* in the ``rho_ss`` eigenbasis,
20+ such a ``rho_0`` has ``c_1 = 0`` by symmetry -- it never populates that mode --
21+ which is trivially fast relaxation, not a Mpemba effect. The default
22+ ``rho_0 = I/d`` and any diagonal state fall in this class. :func:`is_trivial_overlap`
23+ detects it, and :func:`compute_mpemba_layer` withholds the candidate flag so a
24+ canonical amplitude-damping / dephasing system cannot earn an anomalous-Mpemba
25+ label from its default inputs.
726
827D20 -- :func:`expansion_alpha`. Scaling exponent of overlap coefficients
928``|c_n|`` against the index n. Polynomial scaling ``Phi_n ~ exp(alpha L)``
1534import numpy as np
1635import scipy .linalg as sla
1736
18- from .._consts import EPS_GAP
37+ from .._consts import EPS_DIV , EPS_GAP , EPS_HERMITICITY
1938from .._types import MpembaResult
2039from ..numerics .kronecker import vec
2140
2241
42+ def _slowest_mode (
43+ L_super : np .ndarray , * , atol : float = EPS_GAP
44+ ) -> tuple [np .ndarray , np .ndarray ] | None :
45+ """Return ``(l_slow, r_slow)`` for the slowest non-zero mode, or ``None``.
46+
47+ The slowest mode is the non-zero eigenvalue with the largest (least
48+ negative) real part. Returns ``None`` when every mode is a zero mode.
49+ """
50+ L_super = np .asarray (L_super )
51+ eigvals , vl , vr = sla .eig (L_super , left = True , right = True )
52+ nonzero_mask = np .abs (eigvals ) > atol
53+ if not np .any (nonzero_mask ):
54+ return None
55+ eigvals_nz = eigvals [nonzero_mask ]
56+ order = np .argsort (- np .real (eigvals_nz ))
57+ slowest_idx = np .where (nonzero_mask )[0 ][order [0 ]]
58+ return vl [:, slowest_idx ], vr [:, slowest_idx ]
59+
60+
2361def overlap_c1 (
2462 L_super : np .ndarray ,
2563 rho_initial : np .ndarray ,
2664 * ,
2765 atol : float = EPS_GAP ,
2866) -> float :
29- """Magnitude of the slowest-mode overlap ``|c_1|``."""
30- L_super = np .asarray (L_super )
31- eigvals , vl , vr = sla .eig (L_super , left = True , right = True )
32- # Filter the zero mode
33- nonzero_mask = np .abs (eigvals ) > atol
34- eigvals_nz = eigvals [nonzero_mask ]
35- if eigvals_nz .size == 0 :
67+ """Magnitude of the biorthogonal slowest-mode overlap ``|c_1|``.
68+
69+ ``c_1 = <l_1, vec(rho_0)> / <l_1, r_1>``; the denominator is floored at
70+ :data:`liouscope._consts.EPS_DIV` so a genuinely defective (``<l_1, r_1> = 0``)
71+ slow mode yields a large finite value rather than a spurious NaN.
72+ """
73+ mode = _slowest_mode (L_super , atol = atol )
74+ if mode is None :
3675 return 0.0
37- # Slowest non-zero mode = largest real part (least negative)
38- order = np .argsort (- np .real (eigvals_nz ))
39- slowest_idx = np .where (nonzero_mask )[0 ][order [0 ]]
40- l_slow = vl [:, slowest_idx ]
76+ l_slow , r_slow = mode
4177 rho_vec0 = vec (np .asarray (rho_initial ))
42- norm = max (np .linalg .norm (l_slow ), 1.0e-12 )
43- return float (abs (np .vdot (l_slow , rho_vec0 )) / norm )
78+ denom = abs (np .vdot (l_slow , r_slow ))
79+ return float (abs (np .vdot (l_slow , rho_vec0 )) / max (denom , EPS_DIV ))
80+
81+
82+ def _steady_state_eigenprojectors (
83+ rho_steady_state : np .ndarray , * , tol : float = EPS_HERMITICITY
84+ ) -> list [np .ndarray ]:
85+ """Orthogonal projectors onto the distinct eigenspaces of ``rho_ss``.
86+
87+ Grouping degenerate eigenvalues into a single projector makes the sector
88+ decomposition well-defined even when ``rho_ss`` is degenerate or maximally
89+ mixed -- the regime where an individual-eigenvector basis is arbitrary.
90+ """
91+ rho_steady_state = np .asarray (rho_steady_state , dtype = complex )
92+ w , u = np .linalg .eigh (rho_steady_state )
93+ projectors : list [np .ndarray ] = []
94+ i = 0
95+ n = w .size
96+ while i < n :
97+ j = i + 1
98+ while j < n and abs (w [j ] - w [i ]) <= tol :
99+ j += 1
100+ block = u [:, i :j ]
101+ projectors .append (block @ block .conj ().T )
102+ i = j
103+ return projectors
104+
105+
106+ def is_trivial_overlap (
107+ L_super : np .ndarray ,
108+ rho_initial : np .ndarray ,
109+ rho_steady_state : np .ndarray ,
110+ * ,
111+ atol : float = EPS_GAP ,
112+ block_tol : float = 1.0e-9 ,
113+ ) -> bool :
114+ """Is a vanishing slowest-mode overlap symmetry-protected (not Mpemba)?
115+
116+ Returns ``True`` when the zero overlap is *structural*: in the sector
117+ decomposition set by the eigenprojectors ``{P_a}`` of ``rho_ss``, the slowest
118+ mode ``l_1`` and ``rho_0`` occupy **disjoint** blocks ``P_a (.) P_b``. Then
119+ ``rho_0`` can never populate the slowest mode -- its fast relaxation is
120+ trivial, not a fine-tuned Mpemba skip (e.g. a diagonal ``rho_0`` vs a
121+ coherence slowest mode of an amplitude-damping channel).
122+
123+ Using eigen*projectors* (not individual eigenvectors) keeps the test robust
124+ when ``rho_ss`` is degenerate. A maximally mixed ``rho_ss`` collapses to a
125+ single projector (one block), so nothing is structurally decoupled and the
126+ guard does not fire -- such a state has no protecting symmetry sector and is
127+ handled by the (demoted) confidence tier instead.
128+ """
129+ L_super = np .asarray (L_super )
130+ rho_initial = np .asarray (rho_initial , dtype = complex )
131+ rho_steady_state = np .asarray (rho_steady_state , dtype = complex )
132+
133+ mode = _slowest_mode (L_super , atol = atol )
134+ if mode is None :
135+ return False
136+ l_slow , _ = mode
137+ d = rho_steady_state .shape [0 ]
138+ l_mat = l_slow .reshape (d , d , order = "F" )
139+
140+ projectors = _steady_state_eigenprojectors (rho_steady_state )
141+ l_scale = max (float (np .linalg .norm (l_mat )), 1.0 )
142+ rho_scale = max (float (np .linalg .norm (rho_initial )), 1.0 )
143+ for p_a in projectors :
144+ for p_b in projectors :
145+ l_block = float (np .linalg .norm (p_a @ l_mat @ p_b ))
146+ rho_block = float (np .linalg .norm (p_a @ rho_initial @ p_b ))
147+ if l_block > block_tol * l_scale and rho_block > block_tol * rho_scale :
148+ return False # a shared block -> rho_0 can reach the slow mode
149+ return True
44150
45151
46152def expansion_alpha (
@@ -56,7 +162,7 @@ def expansion_alpha(
56162 Returns ``alpha`` (slope). A flat distribution gives ``alpha`` near 0.
57163 """
58164 L_super = np .asarray (L_super )
59- eigvals , vl , _ = sla .eig (L_super , left = True , right = True )
165+ eigvals , vl , vr = sla .eig (L_super , left = True , right = True )
60166 mask = np .abs (eigvals ) > atol
61167 eigvals_nz = eigvals [mask ]
62168 if eigvals_nz .size < 2 :
@@ -67,8 +173,8 @@ def expansion_alpha(
67173 cs : list [float ] = []
68174 for idx in nz_indices [: min (n_modes , len (nz_indices ))]:
69175 l_n = vl [:, idx ]
70- norm = max (np .linalg . norm (l_n ), 1.0e-12 )
71- cs .append (float (abs (np .vdot (l_n , rho_vec0 )) / norm ))
176+ denom = abs (np .vdot (l_n , vr [:, idx ]) )
177+ cs .append (float (abs (np .vdot (l_n , rho_vec0 )) / max ( denom , EPS_DIV ) ))
72178 if len (cs ) < 2 :
73179 return 0.0
74180 log_cs = np .log (np .clip (np .asarray (cs ), 1.0e-30 , None ))
@@ -81,13 +187,26 @@ def compute_mpemba_layer(
81187 L_super : np .ndarray ,
82188 rho_initial : np .ndarray ,
83189 * ,
190+ rho_steady_state : np .ndarray | None = None ,
84191 overlap_threshold : float = 1.0e-4 ,
85192) -> MpembaResult :
86- """Run D19, D20 and flag Mpemba candidacy."""
193+ """Run D19, D20 and flag Mpemba candidacy.
194+
195+ ``is_mpemba_candidate`` is ``True`` only when ``|c_1| < overlap_threshold``
196+ **and** the vanishing overlap is not symmetry-protected. Triviality can only
197+ be assessed when ``rho_steady_state`` is supplied (the standalone default is
198+ ``None``, which preserves the raw overlap test for direct callers).
199+ """
87200 c1 = overlap_c1 (L_super , rho_initial )
88201 alpha = expansion_alpha (L_super , rho_initial )
202+ trivial = (
203+ is_trivial_overlap (L_super , rho_initial , rho_steady_state )
204+ if rho_steady_state is not None
205+ else False
206+ )
89207 return MpembaResult (
90208 overlap_c1 = c1 ,
91- is_mpemba_candidate = c1 < overlap_threshold ,
209+ is_mpemba_candidate = bool ( c1 < overlap_threshold and not trivial ) ,
92210 expansion_alpha = alpha ,
211+ trivial_overlap = trivial ,
93212 )
0 commit comments