amachine.am_solve API documentation

GitLab Repo

# amachine.am_solve

View Source

     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
    88	b = sympy.Matrix([0] * (n - 1) + [1])
    89
    90	# Solve
    91	pi_exact = A.LUsolve(b)
    92	pi_exact = ( Fraction( int(x.p), int(x.q) ) for x in pi_exact )
    93
    94	return tuple(pi_exact)

def solve_for_pi(T: numpy.ndarray, EPS: float = 1e-12) -> numpy.ndarray: View Source

     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]: View Source

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