1010
1111This module represents degenerate spectral information as **projectors**
1212``Π_λ = Q_λ Q_λ^H``. A projector depends only on the subspace, so it is
13- invariant under ``Q_λ → Q_λ · U``. Every downstream spectral observable in TNFR
14- is built from these projectors (see :mod:`tnfr.physics.spectral_certificates`),
15- which makes basis independence structural rather than incidental .
13+ invariant under ``Q_λ → Q_λ · U``. Consumers of this projector API, including
14+ :mod:`tnfr.physics.spectral_certificates`, obtain that basis independence by
15+ construction. Other spectral APIs can retain basis-dependent coordinates .
1616
1717Scope
1818=====
3333
3434import numpy as np
3535
36- try : # scipy is an OPTIONAL dependency; the Schur certificate is gated on it
36+ try : # Retained availability guard; SciPy is a declared core package dependency.
3737 from scipy .linalg import schur as _scipy_schur
3838
3939 _HAS_SCIPY = True
@@ -224,16 +224,23 @@ def matrix_exponential(matrix, *, terms: int = 18) -> np.ndarray:
224224
225225 Self-contained so the non-normal certificates need no SciPy: ``A`` is scaled
226226 by ``2^{-s}`` until its ∞-norm is ``≤ 1``, a truncated Taylor series is
227- evaluated, and the result is squared ``s`` times.
227+ evaluated, and the result is squared ``s`` times. Input must be a finite
228+ square matrix. On the zero-dimensional space, its exponential is the
229+ empty identity, represented with shape ``(0, 0)``.
228230 """
229231 a = np .asarray (matrix , dtype = float )
232+ if a .ndim != 2 or a .shape [0 ] != a .shape [1 ] or not np .all (np .isfinite (a )):
233+ raise ValueError ("matrix must be a finite square array" )
234+ orders = range (1 , terms )
230235 n = a .shape [0 ]
236+ if n == 0 :
237+ return np .eye (0 )
231238 norm = float (np .linalg .norm (a , np .inf ))
232239 s = max (0 , int (np .ceil (np .log2 (norm + 1.0 ))))
233240 b = a / (2 ** s )
234241 result = np .eye (n )
235242 term = np .eye (n )
236- for k in range ( 1 , terms ) :
243+ for k in orders :
237244 term = term @ b / k
238245 result = result + term
239246 for _ in range (s ):
@@ -258,8 +265,11 @@ def transient_gain(matrix, *, t_max: float = 10.0, samples: int = 200) -> float:
258265 stable spectrum alone does not reveal.
259266 """
260267 a = np .asarray (matrix , dtype = float )
268+ times = np .linspace (0.0 , t_max , samples )
269+ if a .shape == (0 , 0 ):
270+ return 0.0 # The only operator on the zero-dimensional space has norm zero.
261271 gain = 0.0
262- for t in np . linspace ( 0.0 , t_max , samples ) :
272+ for t in times :
263273 gain = max (gain , float (np .linalg .norm (matrix_exponential (a * t ), 2 )))
264274 return gain
265275
0 commit comments