Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up linear solvers, add StencilDiagonalMatrix and diagonal(), require pc to be LinearOperator #360

Merged
merged 37 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0bacdd0
Changed the handling of preconditioners in inverse LinearOperators.
camillo81 Dec 6, 2023
11965fa
Moved _check_options() and transpose() from the solver classes to th…
camillo81 Dec 6, 2023
81de0d7
Write new diagonal method for class StencilMatrix
yguclu Dec 14, 2023
3e36e33
Add diagonal method to class BlockLinearOperator
yguclu Dec 14, 2023
f52a291
Use inverse diagonal as preconditioner for P-CG and P-BiCGStab in tests
yguclu Dec 14, 2023
bf198ef
Fix typos
yguclu Dec 15, 2023
1fafaab
Improve efficiency
yguclu Dec 15, 2023
c55d6b6
Fix some tests
yguclu Dec 15, 2023
68b51ac
Fix some tests
yguclu Dec 15, 2023
885f0b3
Pass correct preconditioner in test_2d_multipatch_mapping_maxwell.py
yguclu Jan 8, 2024
10fb4c7
Fix typo in test_2d_multipatch_mapping_maxwell.py
yguclu Jan 8, 2024
9089039
Assemble a DiscreteEquation before accessing the linear_system attribute
yguclu Jan 8, 2024
11aba5e
Pass correct preconditioner in test_api_2d_compatible_spaces
yguclu Jan 8, 2024
15c028d
Fix wrong parameters to inverse() in test_solvers.py
yguclu Jan 10, 2024
9cf1501
Remove obsolete static method jacobi from InverseLinearOperator
yguclu Jan 11, 2024
f3e8807
Clean up inverse() factory function
yguclu Jan 11, 2024
9b2e240
Add recycle option to PBiConjugateGradientStabilized
yguclu Jan 11, 2024
7b947d9
Add recycle option to MinimumResidual
yguclu Jan 11, 2024
fb40372
Merge branch 'devel' into 343-pc-as-linop
yguclu Jan 12, 2024
1867ba1
Introduce class StencilDiagonalMatrix
yguclu Jan 22, 2024
7bac5cc
Make StencilMatrix.diagonal() return a StencilDiagonalMatrix object
yguclu Jan 22, 2024
e90fb50
Do not call diagonal() on empty block
yguclu Jan 23, 2024
f0da5b9
Minor cleanup
yguclu Jan 29, 2024
16ca289
Merge branch 'devel' into 343-pc-as-linop
yguclu Jan 29, 2024
9826636
Remove solver property from InverseLinearOperator base class
yguclu Jan 29, 2024
87727ac
Merge branch 'devel' into 343-pc-as-linop
yguclu Jan 30, 2024
54f4671
Add docstring to linop property of InverseLinearOperator:
yguclu Jan 30, 2024
24b9be2
Set off-diagonal blocks to zero in 'out' argument of diagonal()
yguclu Jan 30, 2024
f514ef4
Improve docstring of PConjugateGradient class
yguclu Jan 30, 2024
ca6593b
Avoid unnecessary transpose operations in test_inverse_transpose_inte…
yguclu Jan 31, 2024
fd61f5f
Clean up test_inverse_transpose_interaction
yguclu Jan 31, 2024
ee5070f
Minor cleanup in psydac.linalg.utilities
yguclu Jan 31, 2024
4b0f821
Move is_real function to psydac.utilities.utils
yguclu Jan 31, 2024
0fa1958
Improve docstring of InverseLinearOperator base class
yguclu Jan 31, 2024
9fe92f1
Remove useless 'options' property from InverseLinearOperator
yguclu Jan 31, 2024
7def827
Improve get_options method of InverseLinearOperator, fix bug
yguclu Jan 31, 2024
1e67613
Fix docstring of get_options
yguclu Jan 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion psydac/api/tests/test_2d_multipatch_mapping_maxwell.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,19 @@ def run_maxwell_2d(uex, f, alpha, domain, *, ncells=None, degree=None, filename=
equation_h = discretize(equation, domain_h, [Vh, Vh], backend=PSYDAC_BACKEND_GPYCCEL)
l2norm_h = discretize(l2norm, domain_h, Vh, backend=PSYDAC_BACKEND_GPYCCEL)

equation_h.set_solver('pcg', pc='jacobi', tol=1e-8)
# Explicitly assemble the linear system
equation_h.assemble()

# Extract the matrix' diagonal to build a Jacobi preconditioner
jacobi_pc = equation_h.linear_system.lhs.diagonal(inverse=True)

# Choose a linear solver and pass any flags to it
equation_h.set_solver('pcg', pc=jacobi_pc, tol=1e-8)

# Solve the linear system and obtain the solution as a FEM field
uh = equation_h.solve()

# Compute the L2 norm of the error
l2_error = l2norm_h.assemble(F=uh)

return l2_error, uh
Expand Down
3 changes: 2 additions & 1 deletion psydac/api/tests/test_api_2d_compatible_spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,8 @@ def run_maxwell_time_harmonic_2d_dir(uex, f, alpha, ncells, degree):
#+++++++++++++++++++++++++++++++

# Solve linear system
M_inv = inverse(M, 'pcg', pc='jacobi', tol=1e-8)
jacobi_pc = M.diagonal(inverse=True)
M_inv = inverse(M, 'pcg', pc=jacobi_pc, tol=1e-8)
sol = M_inv @ b

uh = FemField( Vh, sol )
Expand Down
3 changes: 2 additions & 1 deletion psydac/api/tests/test_api_feec_1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,8 @@ def discrete_energies(self, e, b):
step_ampere_1d = dt * ( M0_dir_inv @ D0_T_dir @ M1 )

elif bc_mode == 'penalization':
M0_M0_bc_inv = inverse(M0+M0_bc, 'pcg', pc='jacobi', **kwargs)
M0_M0_bc = M0 + M0_bc
M0_M0_bc_inv = inverse(M0_M0_bc, 'pcg', pc = M0_M0_bc.diagonal(inverse=True), **kwargs)
step_ampere_1d = dt * ( M0_M0_bc_inv @ D0_T @ M1 )

half_step_faraday_1d = (dt/2) * D0
Expand Down
3 changes: 2 additions & 1 deletion psydac/api/tests/test_api_feec_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,8 @@ def discrete_energies(self, e, b):
M1_inv = inverse(M1, 'cg', **kwargs)
step_ampere_2d = dt * (M1_inv @ D1_T @ M2)
else:
M1_M1_bc_inv = inverse(M1 + M1_bc, 'pcg', pc='jacobi', **kwargs)
M1_M1_bc = M1 + M1_bc
M1_M1_bc_inv = inverse(M1_M1_bc, 'pcg', pc = M1_M1_bc.diagonal(inverse=True), **kwargs)
step_ampere_2d = dt * (M1_M1_bc_inv @ D1_T @ M2)

half_step_faraday_2d = (dt/2) * D1
Expand Down
139 changes: 73 additions & 66 deletions psydac/linalg/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,6 @@ def __new__(cls, domain, codomain=None):
return BlockLinearOperator(domain, domain, blocks)
else:
return super().__new__(cls)


def __init__(self, domain, codomain=None):

Expand Down Expand Up @@ -879,11 +878,39 @@ class InverseLinearOperator(LinearOperator):
Iterative solver for square linear system Ax=b, where x and b belong to (normed)
vector space V.

Parameters
----------
A : psydac.linalg.basic.LinearOperator
Left-hand-side matrix A of linear system.

x0 : psydac.linalg.basic.Vector
First guess of solution for iterative solver (optional).

tol : float
Absolute tolerance for L2-norm of residual r = A*x - b.

maxiter: int
Maximum number of iterations.

verbose : bool
If True, L2-norm of residual r is printed at each iteration.
"""

@property
def space(self):
return self._space
def __init__(self, A, **kwargs):

assert isinstance(A, LinearOperator)
assert A.domain.dimension == A.codomain.dimension
domain = A.codomain
codomain = A.domain

if kwargs['x0'] is None:
kwargs['x0'] = codomain.zeros()

self._A = A
self._domain = domain
self._codomain = codomain
self._options = kwargs
self._check_options(**self._options)

@property
def domain(self):
Expand All @@ -899,11 +926,44 @@ def dtype(self):

@property
def linop(self):
"""
The linear operator L of which this object is the inverse L^{-1}.

The linear operator L can be modified in place, or replaced entirely
through the setter. A substitution should only be made in cases where
no other options are viable, as it breaks the one-to-one map between
the original linear operator L (passed to the constructor) and the
current `InverseLinearOperator` object L^{-1}. Use with extreme care!

"""
return self._A

@linop.setter
def linop(self, a):
assert isinstance(a, LinearOperator)
assert a.domain is self.domain
assert a.codomain is self.codomain
self._A = a
yguclu marked this conversation as resolved.
Show resolved Hide resolved

vcarlier marked this conversation as resolved.
Show resolved Hide resolved
@property
def options(self):
return self._options

def _check_options(self, **kwargs):
for key, value in kwargs.items():

if key == 'x0':
if value is not None:
assert isinstance(value, Vector), "x0 must be a Vector or None"
assert value.space == self.codomain, "x0 belongs to the wrong VectorSpace"
elif key == 'tol':
assert is_real(value), "tol must be a real number"
assert value > 0, "tol must be positive"
elif key == 'maxiter':
assert isinstance(value, int), "maxiter must be an int"
assert value > 0, "maxiter must be positive"
elif key == 'verbose':
assert isinstance(value, bool), "verbose must be a bool"

def toarray(self):
raise NotImplementedError('toarray() is not defined for InverseLinearOperators.')
Expand All @@ -921,69 +981,11 @@ def set_options(self, **kwargs):
self._check_options(**kwargs)
self._options.update(kwargs)

@abstractmethod
def _check_options(self, **kwargs):
pass

@abstractmethod
def transpose(self, conjugate=False):
pass

@staticmethod
def jacobi(A, b, out=None):
"""
Jacobi preconditioner.

A : psydac.linalg.stencil.StencilMatrix | psydac.linalg.block.BlockLinearOperator
Left-hand-side matrix A of linear system.

b : psydac.linalg.stencil.StencilVector | psydac.linalg.block.BlockVector
Right-hand-side vector of linear system.

Returns
-------
x : psydac.linalg.stencil.StencilVector | psydac.linalg.block.BlockVector
Preconditioner solution

"""
from psydac.linalg.block import BlockLinearOperator, BlockVector
from psydac.linalg.stencil import StencilMatrix, StencilVector

# In case A is None we return a zero vector
if A is None:
return b.space.zeros()

# Sanity checks
assert isinstance(A, (StencilMatrix, BlockLinearOperator))
assert isinstance(b, (StencilVector, BlockVector))
assert A.codomain.dimension == A.domain.dimension
assert A.codomain == b.space

#-------------------------------------------------------------
# Handle the case of a block linear system
if isinstance(A, BlockLinearOperator):
if out is not None:
for i, bi in enumerate(b.blocks):
InverseLinearOperator.jacobi(A[i,i], bi, out=out[i])
return out
else:
x = [InverseLinearOperator.jacobi(A[i, i], bi) for i, bi in enumerate(b.blocks)]
y = BlockVector(b.space, blocks=x)
return y
#-------------------------------------------------------------

V = b.space
i = tuple(slice(s, e + 1) for s, e in zip(V.starts, V.ends))

if out is not None:
b.copy(out=out)
out[i] /= A.diagonal()
out.update_ghost_regions()
else:
out = b.copy()
out[i] /= A.diagonal()
out.update_ghost_regions()
return out
cls = type(self)
At = self.linop.transpose(conjugate=conjugate)
options = self._options
return cls(At, **options)

#===============================================================================
class LinearSolver(ABC):
Expand Down Expand Up @@ -1016,3 +1018,8 @@ def solve(self, rhs, out=None):
@property
def T(self):
return self.transpose()


def is_real(x):
from numbers import Number
vcarlier marked this conversation as resolved.
Show resolved Hide resolved
return isinstance(x, Number) and np.isrealobj(x) and not isinstance(x, bool)
48 changes: 46 additions & 2 deletions psydac/linalg/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,49 @@ def __sub__(self, M):
#--------------------------------------
# New properties/methods
#--------------------------------------
def diagonal(self, *, inverse = False, out = None):
"""Get the coefficients on the main diagonal as another BlockLinearOperator object.

Parameters
----------
inverse : bool
If True, get the inverse of the diagonal. (Default: False).

out : BlockLinearOperator
If provided, write the diagonal entries into this matrix. (Default: None).

Returns
-------
BlockLinearOperator
The matrix which contains the main diagonal of self (or its inverse).

"""
# Determine domain and codomain of result
V, W = self.domain, self.codomain
if inverse:
V, W = W, V

# Check the `out` argument, if `None` create a new BlockLinearOperator
if out is not None:
assert isinstance(out, BlockLinearOperator)
assert out.domain is V
assert out.codomain is W

# Set any off-diagonal blocks to zero
for i, j in out.nonzero_block_indices:
if i != j:
out[i, j] = None
else:
out = BlockLinearOperator(V, W)

# Store the diagonal (or its inverse) into `out`
for i, j in self.nonzero_block_indices:
if i == j:
out[i, i] = self[i, i].diagonal(inverse = inverse, out = out[i, i])

return out

# ...
@property
def blocks(self):
""" Immutable 2D view (tuple of tuples) of the linear operator,
Expand Down Expand Up @@ -868,7 +911,8 @@ def __setitem__(self, key, value):
assert value.codomain is self.codomain[i]

self._blocks[i,j] = value


# ...
def transform(self, operation):
"""
Applies an operation on each block in this BlockLinearOperator.
Expand All @@ -881,6 +925,7 @@ def transform(self, operation):
blocks = {ij: operation(Bij) for ij, Bij in self._blocks.items()}
return BlockLinearOperator(self.domain, self.codomain, blocks=blocks)

# ...
def backend(self):
return self._backend

Expand Down Expand Up @@ -909,7 +954,6 @@ def copy(self, out=None):

out.set_backend(self._backend)


# ...
def __imul__(self, a):
for Bij in self._blocks.values():
Expand Down
Loading
Loading