Source code for egttools.numerical.linear_operator
"""
Matrix-free linear operator wrappers and stationary-distribution solvers.
Provides:
- Factory functions that wrap a PairwiseComparisonTransitionOperator as
scipy LinearOperator objects for iterative eigensolvers and Krylov solvers.
- ``stationary_distribution_from_sparse``: convenience solver for an explicit
sparse transition matrix (e.g. from PairwiseComparison.calculate_transition_matrix).
Example usage
-------------
>>> import numpy as np
>>> from egttools.games import NormalFormGame
>>> from egttools.numerical import PairwiseComparisonTransitionOperator
>>> from egttools.numerical.numerical_ import PairwiseComparison
>>> from egttools.numerical.linear_operator import (
... make_transition_operator, make_residual_operator,
... stationary_distribution_from_sparse)
>>>
>>> # Matrix-free path (no Python callbacks)
>>> game = NormalFormGame(...)
>>> op = PairwiseComparisonTransitionOperator(
... population_size=50, game=game, beta=1.0, mu=0.01)
>>> pi = op.compute_stationary_distribution() # power iteration
>>> pi = op.compute_stationary_arpack() # ARPACK (if compiled)
>>>
>>> # Explicit-sparse path (best for moderate state spaces)
>>> pc = PairwiseComparison(50, game)
>>> P = pc.calculate_transition_matrix(beta=1.0, mu=0.01)
>>> pi = stationary_distribution_from_sparse(P)
"""
from __future__ import annotations
import numpy as np
from scipy.sparse.linalg import LinearOperator, eigs
[docs]
def make_transition_operator(operator) -> LinearOperator:
"""Wrap a PairwiseComparisonTransitionOperator as a scipy LinearOperator for P^T.
The returned LinearOperator A satisfies:
- ``A @ x`` computes ``y = P^T x`` (``matvec``)
- ``A.T @ x`` computes ``y = P x`` (``rmatvec``)
The stationary distribution π is the leading eigenvector of P^T
(eigenvalue = 1). Use ``scipy.sparse.linalg.eigs(A, k=1, which='LM')``
to find it.
Parameters
----------
operator : PairwiseComparisonTransitionOperator
Fully constructed C++ transition operator.
Returns
-------
scipy.sparse.linalg.LinearOperator
Shape ``(n, n)`` operator where ``n = operator.size``.
"""
n = operator.size
buf = np.empty(n, dtype=np.float64)
def matvec(x: np.ndarray) -> np.ndarray:
x = np.asarray(x, dtype=np.float64)
operator.apply_transpose(x, buf)
return buf.copy()
def rmatvec(x: np.ndarray) -> np.ndarray:
x = np.asarray(x, dtype=np.float64)
operator.apply(x, buf)
return buf.copy()
return LinearOperator(
shape=(n, n),
matvec=matvec,
rmatvec=rmatvec,
dtype=np.float64,
)
[docs]
def make_residual_operator(operator) -> LinearOperator:
"""Wrap a PairwiseComparisonTransitionOperator as a scipy LinearOperator for (I - P^T).
The returned LinearOperator A satisfies:
- ``A @ x`` computes ``y = (I - P^T) x`` (``matvec``)
Useful for iterative linear solvers (GMRES, LGMRES) that seek π with
``(I - P^T) π = 0``. Because the system is singular, a normalization
constraint must be added externally (e.g. replace one equation with
``Σ π_i = 1``).
Parameters
----------
operator : PairwiseComparisonTransitionOperator
Fully constructed C++ transition operator.
Returns
-------
scipy.sparse.linalg.LinearOperator
Shape ``(n, n)`` operator where ``n = operator.size``.
"""
n = operator.size
buf = np.empty(n, dtype=np.float64)
def matvec(x: np.ndarray) -> np.ndarray:
x = np.asarray(x, dtype=np.float64)
operator.apply_residual(x, buf)
return buf.copy()
return LinearOperator(
shape=(n, n),
matvec=matvec,
dtype=np.float64,
)
[docs]
def stationary_distribution_from_sparse(
P,
tol: float = 1e-12,
max_iter: int = 1000,
) -> np.ndarray:
"""Compute the stationary distribution of an explicit sparse transition matrix.
Uses ``scipy.sparse.linalg.eigs`` (ARPACK) on the transpose of *P* to find
the leading eigenvector, which is the stationary distribution π satisfying
``P^T π = π``.
This is the fastest available local method for moderate state spaces (up to
the RAM limit for storing P) and is the recommended approach when *P* has
already been assembled via ``PairwiseComparison.calculate_transition_matrix``.
Parameters
----------
P : scipy.sparse matrix
Row-stochastic transition matrix of shape ``(n, n)``. Typically the
output of ``PairwiseComparison.calculate_transition_matrix(beta, mu)``.
tol : float
ARPACK convergence tolerance (default 1e-12; 0 → machine precision).
max_iter : int
Maximum number of ARPACK iterations (default 1000).
Returns
-------
numpy.ndarray
Normalised stationary distribution of length ``n``, non-negative and
summing to 1.
Raises
------
scipy.sparse.linalg.ArpackNoConvergence
If ARPACK fails to converge within *max_iter* iterations.
"""
PT = P.T.tocsr()
vals, vecs = eigs(PT, k=1, which="LM", tol=tol, maxiter=max_iter)
pi = vecs[:, 0].real
pi = np.abs(pi)
pi /= pi.sum()
return pi