Source code for bw2calc.jacobi_gmres_lca

from typing import Optional

import numpy as np
import scipy.sparse as sps
from scipy.sparse.linalg import LinearOperator, gmres

from bw2calc.lca import LCA


[docs] class JacobiGMRESLCA(LCA): """Solve ``Ax=b`` with GMRES using a Jacobi preconditioner. The preconditioner is the inverse of the technosphere diagonal, i.e. ``D^-1``. This prior decomposition can significantly improve convergence for certain types of matrices, especially those with dominant diagonal entries. :param demand: Functional unit mapping passed through to :class:`bw2calc.lca.LCA`. :type demand: dict :param data_objs: Datapackages passed through to :class:`bw2calc.lca.LCA`. :type data_objs: iterable :param rtol: Relative tolerance for GMRES convergence. Convergence is checked against a threshold comparable to ``max(rtol * ||b||, atol)``. :type rtol: float :param atol: Absolute tolerance floor for GMRES convergence. :type atol: float :param restart: Number of iterations between GMRES restarts. ``None`` uses SciPy defaults. :type restart: int or None :param maxiter: Maximum number of outer GMRES iterations. :type maxiter: int or None :param use_guess: If ``True``, reuse the previous solution as ``x0`` for subsequent solves in the same instance. :type use_guess: bool """ def __init__( self, *args, rtol: float = 1e-8, atol: float = 0.0, restart: Optional[int] = 50, maxiter: Optional[int] = 300, use_guess: bool = True, **kwargs, ): super().__init__(*args, **kwargs) # GMRES convergence controls.
[docs] self.rtol = rtol
[docs] self.atol = atol
[docs] self.restart = restart
[docs] self.maxiter = maxiter
# When enabled, reuse the previous solution as GMRES initial guess (x0).
[docs] self.use_guess = use_guess
# Cache whether matrix structure cleanup was already done.
[docs] self._matrix_prepared = False
# Cache the Jacobi preconditioner to avoid rebuilding between solves.
[docs] self._cached_preconditioner: Optional[LinearOperator] = None
# Last successful solution vector, used as warm start when `use_guess=True`.
[docs] self.guess = None
def __next__(self) -> None: # Matrix values can change across iteration steps, so invalidate caches. self._matrix_prepared = False self._cached_preconditioner = None super().__next__()
[docs] def load_lci_data(self, nonsquare_ok=False) -> None: super().load_lci_data(nonsquare_ok=nonsquare_ok) # New matrices imply stale solver-side caches. self._matrix_prepared = False self._cached_preconditioner = None self.guess = None
[docs] def _prepare_matrix(self) -> None: # Sparse cleanup is done once per matrix build, then reused. if self._matrix_prepared: return if not sps.isspmatrix(self.technosphere_matrix): raise TypeError("technosphere_matrix must be a SciPy sparse matrix") # GMRES works best with canonical sparse structure. self.technosphere_matrix = self.technosphere_matrix.tocsc(copy=False) self.technosphere_matrix.sum_duplicates() self.technosphere_matrix.eliminate_zeros() self.technosphere_matrix.sort_indices() self._matrix_prepared = True
[docs] def _build_jacobi_preconditioner(self) -> Optional[LinearOperator]: # Reuse preconditioner when solving multiple demands on same matrix. if self._cached_preconditioner is not None: return self._cached_preconditioner diagonal = self.technosphere_matrix.diagonal() # Cannot build Jacobi inverse if any diagonal entry is zero. if np.any(diagonal == 0): return None inverse_diagonal = 1.0 / diagonal # LinearOperator form avoids materializing a dense diagonal inverse matrix. self._cached_preconditioner = LinearOperator( shape=self.technosphere_matrix.shape, matvec=lambda x: inverse_diagonal * x, dtype=self.technosphere_matrix.dtype, ) return self._cached_preconditioner
[docs] def solve_linear_system(self, demand: Optional[np.ndarray] = None) -> np.ndarray: if demand is None: demand = self.demand_array self._prepare_matrix() preconditioner = self._build_jacobi_preconditioner() # Warm start can reduce Krylov iterations for related successive solves. x0 = self.guess if (self.use_guess and self.guess is not None) else None try: # SciPy modern API (`rtol` + `atol`). solution, _ = gmres( self.technosphere_matrix, demand, x0=x0, rtol=self.rtol, atol=self.atol, restart=self.restart, maxiter=self.maxiter, M=preconditioner, ) except TypeError: # Backward compatibility for SciPy versions using `tol`. solution, _ = gmres( self.technosphere_matrix, demand, x0=x0, tol=self.rtol, atol=self.atol, restart=self.restart, maxiter=self.maxiter, M=preconditioner, ) # Match return conventions used elsewhere in bw2calc. solution = np.asarray(solution) if not solution.shape: solution = solution.reshape((1,)) if self.use_guess: # Keep latest solution for the next warm-started call. self.guess = solution return solution