GitLab Repo

amachine.am_solve

 1import numpy as np
 2import scipy as sp
 3import sympy
 4from fractions import Fraction
 5import warnings
 6
 7def solve_for_pi( T : np.ndarray, EPS : float = 1e-12 ) -> np.ndarray :
 8
 9    n = T.shape[0]
10
11    # helper ---------------------------------------------
12
13    def normalize_pi( pi ) :
14        if np.any( pi < -1e-8 ):
15            raise ValueError("Significant negative values in solve result")
16
17        pi = np.clip( pi, a_min=0.0, a_max=None )
18        s = pi.sum()
19
20        if abs(s) < EPS :
21            raise Exception("Degenerate stationary distribution (sum ≈ 0)")
22
23        return pi / s
24
25    # least squares method --------------------------------
26
27    try:
28        A = T.T - np.eye(n)
29        A = np.vstack([np.eye(n) - T.T, np.ones((1, n))])
30        b = np.zeros(n + 1)
31        b[-1] = 1.0
32        pi, *_ = np.linalg.lstsq(A, b, rcond=None)
33        return normalize_pi(pi)
34
35    except (np.linalg.LinAlgError, ValueError):
36        warnings.warn( "Falling back to linalg.solve" )
37
38    # Solve method ----------------------------------------
39
40    try:
41        A = T.T - np.eye(n)
42        A[-1, :] = 1.0
43        b = np.zeros(n)
44        b[-1] = 1.0
45        pi = np.linalg.solve(A, b)
46        return normalize_pi( pi )
47
48    except (np.linalg.LinAlgError, ValueError):
49        warnings.warn( "Falling back to nullspace method" )
50
51    # Nullspace fallback -------------------------------
52
53    try:
54        A = T.T - np.eye(n)
55        ns = sp.linalg.null_space( A )
56
57        if ns.shape[1] != 1:
58            raise ValueError("Unexpected nullspace dimension")
59
60        pi = ns[:, 0]
61        pi *= np.sign(pi[np.argmax(np.abs(pi))])
62        return normalize_pi( pi )
63
64    except (np.linalg.LinAlgError, ValueError):
65        warnings.warn( "Falling back to eigen method" )
66
67    # Eigen fallback ----------------------------------
68
69    eigvals, eigvecs = np.linalg.eig( T.T )
70    idx = np.argmin(np.abs(eigvals - 1))
71    pi = np.real_if_close(eigvecs[:, idx])
72    pi = np.real(pi)
73    pi *= np.sign(pi[np.argmax(np.abs(pi))])
74
75    return normalize_pi( pi )
76
77def solve_for_pi_fractional( T : sympy.Matrix ) -> tuple[Fraction, ...] :
78
79    n = T.shape[0]
80
81    # Setup the system (T.T - I)
82    A = T.transpose() - sympy.Matrix.eye(n)
83
84    # Replace the last row with 1s to enforce sum(pi) = 1
85    for j in range(n):
86        A[n-1, j] = 1
87
88    # Setup the target vector
89    b = sympy.Matrix([0] * (n - 1) + [1])
90
91    # Solve
92    pi_exact = A.LUsolve(b)
93    pi_exact = ( Fraction( int(x.p), int(x.q) ) for x in pi_exact )
94
95    return tuple(pi_exact)
def solve_for_pi(T: numpy.ndarray, EPS: float = 1e-12) -> numpy.ndarray:
 8def solve_for_pi( T : np.ndarray, EPS : float = 1e-12 ) -> np.ndarray :
 9
10    n = T.shape[0]
11
12    # helper ---------------------------------------------
13
14    def normalize_pi( pi ) :
15        if np.any( pi < -1e-8 ):
16            raise ValueError("Significant negative values in solve result")
17
18        pi = np.clip( pi, a_min=0.0, a_max=None )
19        s = pi.sum()
20
21        if abs(s) < EPS :
22            raise Exception("Degenerate stationary distribution (sum ≈ 0)")
23
24        return pi / s
25
26    # least squares method --------------------------------
27
28    try:
29        A = T.T - np.eye(n)
30        A = np.vstack([np.eye(n) - T.T, np.ones((1, n))])
31        b = np.zeros(n + 1)
32        b[-1] = 1.0
33        pi, *_ = np.linalg.lstsq(A, b, rcond=None)
34        return normalize_pi(pi)
35
36    except (np.linalg.LinAlgError, ValueError):
37        warnings.warn( "Falling back to linalg.solve" )
38
39    # Solve method ----------------------------------------
40
41    try:
42        A = T.T - np.eye(n)
43        A[-1, :] = 1.0
44        b = np.zeros(n)
45        b[-1] = 1.0
46        pi = np.linalg.solve(A, b)
47        return normalize_pi( pi )
48
49    except (np.linalg.LinAlgError, ValueError):
50        warnings.warn( "Falling back to nullspace method" )
51
52    # Nullspace fallback -------------------------------
53
54    try:
55        A = T.T - np.eye(n)
56        ns = sp.linalg.null_space( A )
57
58        if ns.shape[1] != 1:
59            raise ValueError("Unexpected nullspace dimension")
60
61        pi = ns[:, 0]
62        pi *= np.sign(pi[np.argmax(np.abs(pi))])
63        return normalize_pi( pi )
64
65    except (np.linalg.LinAlgError, ValueError):
66        warnings.warn( "Falling back to eigen method" )
67
68    # Eigen fallback ----------------------------------
69
70    eigvals, eigvecs = np.linalg.eig( T.T )
71    idx = np.argmin(np.abs(eigvals - 1))
72    pi = np.real_if_close(eigvecs[:, idx])
73    pi = np.real(pi)
74    pi *= np.sign(pi[np.argmax(np.abs(pi))])
75
76    return normalize_pi( pi )
def solve_for_pi_fractional( T: sympy.matrices.dense.MutableDenseMatrix) -> tuple[fractions.Fraction, ...]:
78def solve_for_pi_fractional( T : sympy.Matrix ) -> tuple[Fraction, ...] :
79
80    n = T.shape[0]
81
82    # Setup the system (T.T - I)
83    A = T.transpose() - sympy.Matrix.eye(n)
84
85    # Replace the last row with 1s to enforce sum(pi) = 1
86    for j in range(n):
87        A[n-1, j] = 1
88
89    # Setup the target vector
90    b = sympy.Matrix([0] * (n - 1) + [1])
91
92    # Solve
93    pi_exact = A.LUsolve(b)
94    pi_exact = ( Fraction( int(x.p), int(x.q) ) for x in pi_exact )
95
96    return tuple(pi_exact)