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