Source code for struphy.linear_algebra.schur_solver

from feectools.linalg.basic import IdentityOperator, LinearOperator, Vector
from feectools.linalg.block import BlockLinearOperator, BlockVector
from feectools.linalg.solvers import inverse
from line_profiler import profile
from scope_profiler import ProfileManager

from struphy.linear_algebra.solver import SolverParameters


[docs] class SchurSolver: r"""Solves for :math:`x^{n+1}` in the block system .. math:: \begin{pmatrix} A & \Delta t\, B \\ \Delta t\, C & \mathbb I \end{pmatrix} \begin{pmatrix} x^{n+1} \\ y^{n+1} \end{pmatrix} = \begin{pmatrix} A & - \Delta t\, B \\ - \Delta t\, C & \mathbb I \end{pmatrix} \begin{pmatrix} x^n \\ y^n \end{pmatrix} using the Schur complement :math:`S = A - \Delta t^2 BC`, where :math:`\mathbb I` is the identity matrix and :math:`(x^n, y^n)` is given. The solution is .. math:: x^{n+1} = S^{-1} \left[ (A + \Delta t^2 BC) \, x^n - 2 \Delta t\, B \, y^n \right] \,. The caller passes :math:`B y^n` and computes :math:`y^{n+1} = y^n - \Delta t\, C (x^{n+1} + x^n)` itself. Parameters ---------- A : LinearOperator Upper left block :math:`A`. BC : LinearOperator Product :math:`BC` of the off-diagonal blocks (same domain and codomain as :math:`A`). solver_name : str Name of the iterative solver, passed to ``feectools.linalg.solvers.inverse`` (e.g. ``"pcg"``, ``"cg"``, ``"pbicgstab"``, ``"gmres"``). precond : LinearOperator, optional Preconditioner of the Schur complement :math:`S` (can be replaced later with :attr:`precond`). solver_params : SolverParameters, optional Solver controls (``tol``, ``maxiter``, ...); defaults to ``SolverParameters()``. """ def __init__( self, A: LinearOperator, BC: LinearOperator, solver_name: str, precond=None, # TODO: add Preconditioner base class solver_params: SolverParameters = None, ): assert isinstance(A, LinearOperator) assert isinstance(BC, LinearOperator) assert A.domain == BC.domain assert A.codomain == BC.codomain if solver_params is None: solver_params = SolverParameters() # linear operators self._A = A self._BC = BC # Allocate memory for matrices used in solving the Schur system self._schur = A.copy() self._rhs_m = A.copy() # initialize solver with dummy matrix A self._solver_name = solver_name kwargs = solver_params.__dict__.copy() kwargs.pop("info") if precond is not None: kwargs["pc"] = precond self._solver_kwargs = kwargs self._solver = inverse(A, solver_name, **kwargs) # right-hand side vector (avoids temporary memory allocation!) self._rhs = A.codomain.zeros() @property def A(self): """Upper left block :math:`A`.""" return self._A @property def BC(self): """Product :math:`BC` of the off-diagonal blocks.""" return self._BC @property def precond(self): """Preconditioner of the Schur system (None without preconditioner).""" return self._solver_kwargs.get("pc") @precond.setter def precond(self, pc): """Replace the preconditioner, e.g. by one of the Schur system for the current dt (built at the first solve).""" self._solver_kwargs.pop("pc", None) if pc is not None: self._solver_kwargs["pc"] = pc self._solver = inverse(self._A, self._solver_name, **self._solver_kwargs) @A.setter def A(self, a): """Upper left block :math:`A`.""" self._A = a @BC.setter def BC(self, bc): """Product :math:`BC` of the off-diagonal blocks.""" self._BC = bc @profile @ProfileManager.profile("solve: SchurSolver") def __call__(self, xn, Byn, dt, out=None): """Solves the 2x2 block matrix linear system. Parameters ---------- xn : feectools.linalg.basic.Vector Solution from previous time step. Byn : feectools.linalg.basic.Vector The product B*yn. dt : float Time step size. out : feectools.linalg.basic.Vector, optional If given, the converged solution will be written into this vector (in-place). Returns ------- out : feectools.linalg.basic.Vector Converged solution. info : dict Convergence information. """ assert isinstance(xn, Vector) assert isinstance(Byn, Vector) assert xn.space == self._A.domain assert Byn.space == self._A.codomain # left- and right-hand side operators self._schur *= 0.0 self._schur += self._BC self._schur *= -(dt**2) self._schur += self._A self._rhs_m *= 0.0 self._rhs_m += self._BC self._rhs_m *= dt**2 self._rhs_m += self._A # use setter to update lhs matrix self._solver.linop = self._schur # right-hand side vector rhs = 2*dt*[ rhs_m/(2*dt) @ xn - Byn ] (in-place!) rhs = self._rhs_m.dot(xn, out=self._rhs) rhs /= 2 * dt rhs -= Byn rhs *= 2 * dt # solve linear system (in-place if out is not None) x = self._solver.dot(rhs, out=out) return x, self._solver._info
[docs] class SchurSolverFull: r"""Solves the block system .. math:: \begin{pmatrix} A & B \\ C & \mathbb I \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} b_x \\ b_y \end{pmatrix} using the Schur complement :math:`S = A - BC`, where :math:`\mathbb I` is the identity matrix and :math:`(b_x, b_y)^\top` is given. The solution is .. math:: x &= S^{-1} \, (b_x - B b_y ) \,, \\ y &= b_y - C x \,. Parameters ---------- M : BlockLinearOperator Block matrix :math:`[[A, B], [C, \mathbb I]]`. solver_name : str Name of the iterative solver, passed to ``feectools.linalg.solvers.inverse`` (e.g. ``"pcg"``, ``"cg"``, ``"pbicgstab"``, ``"gmres"``). **solver_params : Keyword arguments of the chosen solver; must contain ``pc`` (the preconditioner, None for none). """ def __init__(self, M, solver_name, **solver_params): assert isinstance(M, BlockLinearOperator) assert M.domain == M.codomain # solve square system # initialize solver with dummy matrix A self._solver_name = solver_name if solver_params["pc"] is None: solver_params.pop("pc") self._M = M self._A = M[0, 0] self._B = M[0, 1] self._C = M[1, 0] assert isinstance(M[1, 1], IdentityOperator) self._S = self._A - self._B @ self._C self._solver = inverse(self._S, solver_name, **solver_params) # right-hand side vector (avoids temporary memory allocation!) self._rhs = self._A.codomain.zeros()
[docs] @profile @ProfileManager.profile("solve: SchurSolverFull") def dot(self, v, out=None): """Solves the 2x2 block matrix linear system. Parameters ---------- v : feectools.linalg.basic.Vector Left hand side of the system. out : feectools.linalg.basic.Vector, optional If given, the converged solution will be written into this vector (in-place). Returns ------- out : feectools.linalg.block.BLockVector Converged solution. info : dict Convergence information. """ assert isinstance(v, BlockVector) assert v.space == self._M.domain if out is None: out = self._M.codomain.zeros() else: assert out.space == self._M.codomain bx = v[0] by = v[1] # right-hand side vector rhs bx - B by rhs = self._B.dot(by, out=self._rhs) rhs *= -1 rhs += bx # solve linear system (in-place if out is not None) x = self._solver.dot(rhs, out=out[0]) y = self._C.dot(x, out=out[1]) y *= -1 y += by return out
[docs] class SchurSolverFull3: r"""Solves the block system .. math:: \begin{pmatrix} A & B & D \\ C & \mathbb I & 0 \\ E & 0 & \mathbb I \end{pmatrix} \begin{pmatrix} x \\ y \\ z \end{pmatrix} = \begin{pmatrix} b_x \\ b_y \\ b_z \end{pmatrix} using the Schur complement :math:`S = A - BC - DE`, where :math:`\mathbb I` is the identity matrix and :math:`(b_x, b_y, b_z)^\top` is given. The solution is .. math:: x &= S^{-1} \, (b_x - B b_y - D b_z) \,, \\ y &= b_y - C x \,, \\ z &= b_z - E x \,. Parameters ---------- M : BlockLinearOperator Block matrix :math:`[[A, B, D], [C, \mathbb I, 0], [E, 0, \mathbb I]]`. solver_name : str Name of the iterative solver, passed to ``feectools.linalg.solvers.inverse`` (e.g. ``"pcg"``, ``"cg"``, ``"pbicgstab"``, ``"gmres"``). **solver_params : Keyword arguments of the chosen solver; must contain ``pc`` (the preconditioner, None for none). """ def __init__(self, M, solver_name, **solver_params): assert isinstance(M, BlockLinearOperator) assert M.domain == M.codomain # solve square system # initialize solver with dummy matrix A self._solver_name = solver_name if solver_params["pc"] is None: solver_params.pop("pc") self._M = M self._A = M[0, 0] self._B = M[0, 1] self._C = M[1, 0] self._D = M[0, 2] self._E = M[2, 0] NoneType = type(None) assert isinstance(M[1, 1], IdentityOperator) assert isinstance(M[2, 2], IdentityOperator) assert isinstance(M[1, 2], NoneType) assert isinstance(M[2, 1], NoneType) self._S = self._A - self._B @ self._C - self._D @ self._E self._solver = inverse(self._S, solver_name, **solver_params) # right-hand side vector (avoids temporary memory allocation!) self._rhs = self._A.codomain.zeros() self._rhs2 = self._A.codomain.zeros()
[docs] @profile @ProfileManager.profile("solve: SchurSolverFull3") def dot(self, v, out=None): """Solves the 3x3 block matrix linear system. Parameters ---------- v : feectools.linalg.basic.Vector Left hand side of the system. out : feectools.linalg.basic.Vector, optional If given, the converged solution will be written into this vector (in-place). Returns ------- out : feectools.linalg.block.BLockVector Converged solution. info : dict Convergence information. """ assert isinstance(v, BlockVector) assert v.space == self._M.domain if out is None: out = self._M.codomain.zeros() else: assert out.space == self._M.codomain bx = v[0] by = v[1] bz = v[2] # right-hand side vector rhs bx - B by rhs = self._B.dot(by, out=self._rhs) rhs *= -1 rhs -= self._D.dot(bz, out=self._rhs2) rhs += bx # solve linear system (in-place if out is not None) x = self._solver.dot(rhs, out=out[0]) y = self._C.dot(x, out=out[1]) y *= -1 y += by z = self._E.dot(x, out=out[2]) z *= -1 z += bz return out