Source code for gasm.convergence

"""Convergence criteria for the GASM iterative procedure.

The reference article (eq. 30) fixes the number of iterations to
``k_tilde = min(diam_A, diam_B)``. Supp. Fig. S3 shows the accuracy usually
plateaus well before that bound, so this module also provides an adaptive
early-stop criterion that monitors the stabilisation of the row-wise argmax
assignment -- a cheap surrogate for the final LAP that avoids running the LAP at
every iteration -- capped by the diameter for safety.
"""

from __future__ import annotations

import numpy as np


[docs] class ConvergenceMonitor: """Track convergence of the vertex score matrix across iterations. Parameters ---------- mode: ``'adaptive'`` (default) for early stopping, or ``'diameter'`` to reproduce the fixed-iteration behaviour of the article. diameter_cap: Hard upper bound on the number of iterations ``k_tilde`` (eq. 30). max_iterations: Optional manual override of the hard cap. tol: Relative Frobenius-norm tolerance for the early-stop criterion. patience: Number of consecutive iterations with an unchanged argmax assignment required to declare convergence. floor: Minimum number of iterations before early stopping is allowed. """ def __init__( self, mode: str = "adaptive", diameter_cap: int = 1, max_iterations: int | None = None, tol: float = 1e-6, patience: int = 2, floor: int = 2, ): if mode not in ("adaptive", "diameter"): raise ValueError("convergence must be 'adaptive' or 'diameter'.") self.mode = mode cap = diameter_cap if max_iterations is None else max_iterations self.cap = max(int(cap), 1) self.tol = tol self.patience = patience self.floor = max(floor, 1) self._prev_argmax: np.ndarray | None = None self._prev_X: np.ndarray | None = None self._stable = 0 self.iterations = 0 @property def max_steps(self) -> int: """Maximum number of update iterations that will be performed.""" return self.cap
[docs] def update(self, k: int, X: np.ndarray) -> bool: """Register iteration ``k`` and return ``True`` if iteration should stop. Parameters ---------- k: Current iteration index (``>= 1``). X: Current vertex score matrix. """ self.iterations = k if k >= self.cap: return True if self.mode == "diameter": return False if k < self.floor: self._prev_argmax = X.argmax(axis=1) self._prev_X = X return False argmax = X.argmax(axis=1) stable = False if self._prev_argmax is not None and np.array_equal(argmax, self._prev_argmax): self._stable += 1 else: self._stable = 0 if self._prev_X is not None: denom = np.linalg.norm(X) if denom > 0: rel = np.linalg.norm(X - self._prev_X) / denom if rel < self.tol: stable = True self._prev_argmax = argmax self._prev_X = X return stable or self._stable >= self.patience