Source code for psydac.linalg.block

# coding: utf-8
#
# Copyright 2018 Jalal Lakhlili, Yaman Güçlü

import numpy as np

from types import MappingProxyType
from scipy.sparse import bmat, lil_matrix

from psydac.linalg.basic    import VectorSpace, Vector, LinearOperator
from psydac.linalg.stencil  import StencilMatrix
from psydac.ddm.cart        import InterfaceCartDecomposition
from psydac.ddm.utilities   import get_data_exchanger

__all__ = ('BlockVectorSpace', 'BlockVector', 'BlockLinearOperator')

#===============================================================================
class BlockVectorSpace(VectorSpace):
    """
    Product Vector Space V of two Vector Spaces (V1,V2) or more.

    Parameters
    ----------
    *spaces : psydac.linalg.basic.VectorSpace
        A list of Vector Spaces.

    """
    def __new__(cls, *spaces, connectivity=None):

        # Check that all input arguments are vector spaces
        if not all(isinstance(Vi, VectorSpace) for Vi in spaces):
            raise TypeError('All input spaces must be VectorSpace objects')

        # If no spaces are passed, raise an error
        if len(spaces) == 0:
            raise ValueError('Cannot create a BlockVectorSpace of zero spaces')

        # If only one space is passed, return it without creating a new object
        if len(spaces) == 1:
            return spaces[0]

        # Create a new BlockVectorSpace object
        return VectorSpace.__new__(cls)

    # ...
    def __init__(self, *spaces, connectivity=None):

        # Store spaces in a Tuple, because they will not be changed
        self._spaces = tuple(spaces)

        if all(np.dtype(s.dtype)==np.dtype(spaces[0].dtype) for s in spaces):
            self._dtype  = spaces[0].dtype
        else:
            raise NotImplementedError("The matrices domains don't have the same data type.")

        self._connectivity = connectivity or {}
        self._connectivity_readonly = MappingProxyType(self._connectivity)

    #--------------------------------------
    # Abstract interface
    #--------------------------------------
    @property
    def dimension(self):
        """
        The dimension of a product space V = (V1, V2, ...] is the cardinality
        (i.e. the number of vectors) of a basis of V over its base field.

        """
        return sum(Vi.dimension for Vi in self._spaces)

    # ...
    @property
    def dtype(self):
        return self._dtype

    # ...
    def zeros(self):
        """
        Get a copy of the null element of the product space V = [V1, V2, ...]

        Returns
        -------
        null : BlockVector
            A new vector object with all components equal to zero.

        """
        return BlockVector(self, [Vi.zeros() for Vi in self._spaces])

    # ...
    def inner(self, x, y):
        """
        Evaluate the inner vector product between two vectors of this space V.

        If the field of V is real, compute the classical scalar product.
        If the field of V is complex, compute the classical sesquilinear
        product with linearity on the second vector.

        TODO [YG 01.05.2025]: Currently, the first vector is conjugated. We
        want to reverse this behavior in order to align with the convention
        of FEniCS.

        Parameters
        ----------
        x : Vector
            The first vector in the scalar product. In the case of a complex
            field, the inner product is antilinear w.r.t. this vector (hence
            this vector is conjugated).

        y : Vector
            The second vector in the scalar product. The inner product is
            linear w.r.t. this vector.

        Returns
        -------
        float | complex
            The scalar product of the two vectors. Note that inner(x, x) is
            a non-negative real number which is zero if and only if x = 0.

        """

        assert isinstance(x, BlockVector)
        assert isinstance(y, BlockVector)
        assert x.space is self
        assert y.space is self
        return sum(Vi.inner(xi, yi) for Vi, xi, yi in zip(self.spaces, x.blocks, y.blocks))

    #...
    def axpy(self, a, x, y):
        """
        Increment the vector y with the a-scaled vector x, i.e. y = a * x + y,
        provided that x and y belong to the same vector space V (self).
        The scalar value a may be real or complex, depending on the field of V.

        Parameters
        ----------
        a : scalar
            The scaling coefficient needed for the operation.

        x : BlockVector
            The vector which is not modified by this function.

        y : BlockVector
            The vector modified by this function (incremented by a * x).
        """

        assert isinstance(x, BlockVector)
        assert isinstance(y, BlockVector)
        assert x.space is self
        assert y.space is self

        for Vi, xi, yi in zip(self.spaces, x.blocks, y.blocks):
            Vi.axpy(a, xi, yi)

        x._sync = x._sync and y._sync

    #--------------------------------------
    # Other properties/methods
    #--------------------------------------
    @property
    def spaces(self):
        return self._spaces

    @property
    def parallel(self):
        """ Returns True if the memory is distributed."""
        return self._spaces[0].parallel

    @property
    def starts(self):
        return [s.starts for s in self._spaces]

    @property
    def ends(self):
        return [s.ends for s in self._spaces]

    @property
    def pads(self):
        return self._spaces[0].pads

    @property
    def n_blocks(self):
        return len(self._spaces)

    @property
    def connectivity(self):
        return self._connectivity_readonly

    def __getitem__(self, key):
        return self._spaces[key]

#===============================================================================
[docs] class BlockVector(Vector): """ Block of Vectors, which is an element of a BlockVectorSpace. Parameters ---------- V : psydac.linalg.block.BlockVectorSpace Space to which the new vector belongs. blocks : list or tuple (psydac.linalg.basic.Vector) List of Vector objects, belonging to the correct spaces (optional). """ def __init__(self, V, blocks=None): assert isinstance(V, BlockVectorSpace) self._space = V # We store the blocks in a List so that we can change them later. if blocks: # Verify that vectors belong to correct spaces and store them assert isinstance(blocks, (list, tuple)) assert all((isinstance(b, Vector)) for b in blocks) assert all((Vi is bi.space) for Vi,bi in zip(V.spaces, blocks)) self._blocks = list(blocks) else: # TODO: Each block is a 'zeros' vector of the correct space for now, # but in the future we would like 'empty' vectors of the same space. self._blocks = [Vi.zeros() for Vi in V.spaces] # TODO: distinguish between different directions self._sync = False self._data_exchangers = {} self._interface_buf = {} if not V.parallel: return # Prepare the data exchangers for the interface data for i, j in V.connectivity: ((axis_i, ext_i),(axis_j, ext_j)) = V.connectivity[i, j] Vi = V.spaces[i] Vj = V.spaces[j] self._data_exchangers[i, j] = [] if isinstance(Vi, BlockVectorSpace) and isinstance(Vj, BlockVectorSpace): # case of a system of equations for k, (Vik, Vjk) in enumerate(zip(Vi.spaces, Vj.spaces)): cart_i = Vik.cart cart_j = Vjk.cart if cart_i.is_comm_null and cart_j.is_comm_null: continue if not cart_i.is_comm_null and not cart_j.is_comm_null: continue if not (axis_i, ext_i) in Vik.interfaces: continue cart_ij = Vik.interfaces[axis_i, ext_i].cart assert isinstance(cart_ij, InterfaceCartDecomposition) self._data_exchangers[i, j].append(get_data_exchanger(cart_ij, self.dtype)) elif not isinstance(Vi, BlockVectorSpace) and not isinstance(Vj, BlockVectorSpace): # case of scalar equations cart_i = Vi.cart cart_j = Vj.cart if cart_i.is_comm_null and cart_j.is_comm_null: continue if not cart_i.is_comm_null and not cart_j.is_comm_null: continue if not (axis_i, ext_i) in Vi.interfaces: continue cart_ij = Vi.interfaces[axis_i, ext_i].cart assert isinstance(cart_ij, InterfaceCartDecomposition) self._data_exchangers[i, j].append(get_data_exchanger(cart_ij, self.dtype)) else: raise NotImplementedError("This case is not treated") for i, j in V.connectivity: if len(self._data_exchangers.get((i, j), [])) == 0: self._data_exchangers.pop((i, j), None) #-------------------------------------- # Abstract interface #-------------------------------------- @property def space(self): """ Vector space to which this vector belongs. """ return self._space # ...
[docs] def toarray(self, order='C'): """ Convert to Numpy 1D array. """ return np.concatenate([bi.toarray(order=order) for bi in self._blocks])
#...
[docs] def copy(self, out=None): if self is out: return self w = out or BlockVector(self._space)#, [b.copy() for b in self._blocks]) for n, b in enumerate(self._blocks): b.copy(out=w[n]) w._sync = self._sync return w
#...
[docs] def conjugate(self, out=None): if out is not None: assert isinstance(out, BlockVector) assert out.space is self.space else: out = BlockVector(self.space) for (Lij, Lij_out) in zip(self.blocks, out.blocks): Lij.conjugate(out=Lij_out) out._sync = self._sync return out
#... def __neg__(self): w = BlockVector(self._space, [-b for b in self._blocks]) w._sync = self._sync return w #... def __mul__(self, a): w = BlockVector(self._space, [b * a for b in self._blocks]) w._sync = self._sync return w #... def __add__(self, v): assert isinstance(v, BlockVector) assert v._space is self._space w = BlockVector(self._space, [b1 + b2 for b1, b2 in zip(self._blocks, v._blocks)]) w._sync = self._sync and v._sync return w #... def __sub__(self, v): assert isinstance(v, BlockVector) assert v._space is self._space w = BlockVector(self._space, [b1 - b2 for b1, b2 in zip(self._blocks, v._blocks)]) w._sync = self._sync and v._sync return w #... def __imul__(self, a): for b in self._blocks: b *= a return self #... def __iadd__(self, v): assert isinstance(v, BlockVector) assert v._space is self._space for b1, b2 in zip(self._blocks, v._blocks): b1 += b2 self._sync = self._sync and v._sync return self #... def __isub__(self, v): assert isinstance(v, BlockVector) assert v._space is self._space for b1, b2 in zip(self._blocks, v._blocks): b1 -= b2 self._sync = self._sync and v._sync return self #-------------------------------------- # Other properties/methods #-------------------------------------- @property def blocks(self): return tuple(self._blocks) #... @property def n_blocks(self): return len(self._blocks) # ... def __getitem__(self, key): return self._blocks[key] # ... def __setitem__(self, key, value): assert value.space == self.space[key] assert isinstance(value, Vector) self._blocks[key] = value # ... @property def ghost_regions_in_sync(self): return self._sync # ... # NOTE: this property must be set collectively @ghost_regions_in_sync.setter def ghost_regions_in_sync(self, value): assert isinstance(value, bool) self._sync = value for vi in self.blocks: vi.ghost_regions_in_sync = value # ...
[docs] def update_ghost_regions(self): req = self.start_update_interface_ghost_regions() for vi in self.blocks: vi.update_ghost_regions() self.end_update_interface_ghost_regions(req) # Flag ghost regions as up-to-date self._sync = True
[docs] def start_update_interface_ghost_regions(self): self._collect_interface_buf() req = {} for (i, j) in self._data_exchangers: req[i, j] = [data_ex.start_update_ghost_regions(*bufs) for bufs, data_ex in zip(self._interface_buf[i, j], self._data_exchangers[i, j])] return req
[docs] def end_update_interface_ghost_regions(self, req): for (i, j) in self._data_exchangers: for data_ex, bufs, req_ij in zip(self._data_exchangers[i, j], self._interface_buf[i, j], req[i, j]): data_ex.end_update_ghost_regions(req_ij)
def _collect_interface_buf(self): V = self.space if not V.parallel:return for i, j in V.connectivity: if (i, j) not in self._data_exchangers: continue ((axis_i, ext_i), (axis_j, ext_j)) = V.connectivity[i, j] Vi = V.spaces[i] Vj = V.spaces[j] # The process that owns the patch i will use block i to send data and receive in block j self._interface_buf[i, j] = [] if isinstance(Vi, BlockVectorSpace) and isinstance(Vj, BlockVectorSpace): # case of a system of equations for k, (Vik, Vjk) in enumerate(zip(Vi.spaces, Vj.spaces)): cart_i = Vik.cart cart_j = Vjk.cart buf = [None]*2 if cart_i.is_comm_null: buf[0] = self._blocks[i]._blocks[k]._interface_data[axis_i, ext_i] else: buf[0] = self._blocks[i]._blocks[k]._data if cart_j.is_comm_null: buf[1] = self._blocks[j]._blocks[k]._interface_data[axis_j, ext_j] else: buf[1] = self._blocks[j]._blocks[k]._data self._interface_buf[i,j].append(tuple(buf)) elif not isinstance(Vi, BlockVectorSpace) and not isinstance(Vj, BlockVectorSpace): # case of scalar equations cart_i = Vi.cart cart_j = Vj.cart if cart_i.is_comm_null: read_buffer = self._blocks[i]._interface_data[axis_i, ext_i] else: read_buffer = self._blocks[i]._data if cart_j.is_comm_null: write_buffer = self._blocks[j]._interface_data[axis_j, ext_j] else: write_buffer = self._blocks[j]._data self._interface_buf[i, j].append((read_buffer, write_buffer)) # ...
[docs] def exchange_assembly_data(self): for vi in self.blocks: vi.exchange_assembly_data()
# ...
[docs] def toarray_local(self, order='C'): """ Convert to petsc Nest vector. """ blocks = [v.toarray_local(order=order) for v in self._blocks] return np.block([blocks])[0]
# ...
[docs] def topetsc(self): """ Convert to petsc data structure. """ from psydac.linalg.topetsc import vec_topetsc vec = vec_topetsc( self ) return vec
#===============================================================================
[docs] class BlockLinearOperator(LinearOperator): """ Linear operator that can be written as blocks of other Linear Operators. Either the domain or the codomain of this operator, or both, should be of class BlockVectorSpace. Parameters ---------- V1 : psydac.linalg.block.VectorSpace Domain of the new linear operator. V2 : psydac.linalg.block.VectorSpace Codomain of the new linear operator. blocks : dict | (list of lists) | (tuple of tuples) LinearOperator objects (optional). a) 'blocks' can be dictionary with . key = tuple (i, j), where i and j are two integers >= 0 . value = corresponding LinearOperator Lij b) 'blocks' can be list of lists (or tuple of tuples) where blocks[i][j] is the LinearOperator Lij (if None, we assume null operator) """ def __init__(self, V1, V2, blocks=None): assert isinstance(V1, VectorSpace) assert isinstance(V2, VectorSpace) if not (isinstance(V1, BlockVectorSpace) or isinstance(V2, BlockVectorSpace)): raise TypeError("Either domain or codomain must be of type BlockVectorSpace") self._domain = V1 self._codomain = V2 self._blocks = {} self._nrows = V2.n_blocks if isinstance(V2, BlockVectorSpace) else 1 self._ncols = V1.n_blocks if isinstance(V1, BlockVectorSpace) else 1 # Store blocks in dict (hence they can be manually changed later) if blocks: if isinstance(blocks, dict): for (i, j), Lij in blocks.items(): self[i, j] = Lij elif isinstance(blocks, (list, tuple)): blocks = np.array(blocks, dtype=object) for (i, j), Lij in np.ndenumerate(blocks): self[i, j] = Lij else: raise ValueError( "Blocks can only be given as dict or 2D list/tuple." ) self._args = {} self._blocks_as_args = self._blocks self._increment = self._codomain.zeros() self._args['inc'] = self._increment self._args['n_rows'] = self._nrows self._args['n_cols'] = self._ncols self._func = self._dot self._sync = False self._backend = None #-------------------------------------- # Abstract interface #-------------------------------------- @property def domain(self): return self._domain # ... @property def codomain(self): return self._codomain # ... @property def dtype(self): return self.domain.dtype
[docs] def conjugate(self, out=None): if out is not None: assert isinstance(out, BlockLinearOperator) assert out.domain is self.domain assert out.codomain is self.codomain else: out = BlockLinearOperator(self.domain, self.codomain) for (i, j), Lij in self._blocks.items(): assert isinstance(Lij, (StencilMatrix, BlockLinearOperator)) if out[i,j]==None: out[i, j] = Lij.conjugate() else: Lij.conjugate(out=out[i,j]) return out
[docs] def conj(self, out=None): return self.conjugate(out=out)
# NOTE [YG 27.03.2023]: # NOTE as part of PR 279, this method was added to facilitate comparisons in tests, # NOTE but then commented out as deemed unnecessary. # def __eq__(self, B): # """ # Return True if self and B are mathematically the same, else return False. # Also returns False if at least one block is not the same object and the entries can't be accessed and compared using toarray(). # # """ # assert isinstance(B, BlockLinearOperator) # # if self is B: # return True # # nrows = self._nrows # ncols = self._ncols # if not ((B.n_block_cols == ncols) & (B.n_block_rows == nrows)): # return False # # for i in range(nrows): # for j in range(ncols): # A_ij = self[i, j] # B_ij = B[i, j] # if not ( A_ij is B_ij ): # if not (((A_ij is None) or (isinstance(A_ij, ZeroOperator))) & ((B_ij is None) or (isinstance(B_ij, ZeroOperator)))): # if not ( np.array_equal(A_ij.toarray(), B_ij.toarray()) ): # return False # return True # ...
[docs] def tosparse(self, **kwargs): """ Convert to any Scipy sparse matrix format. """ # Shortcuts nrows = self.n_block_rows ncols = self.n_block_cols # Utility functions: get domain of blocks on column j, get codomain of blocks on row i block_domain = (lambda j: self.domain [j]) if ncols > 1 else (lambda j: self.domain) block_codomain = (lambda i: self.codomain[i]) if nrows > 1 else (lambda i: self.codomain) # Convert all blocks to Scipy sparse format blocks_sparse = [[None for j in range(ncols)] for i in range(nrows)] for i in range(nrows): for j in range(ncols): if (i, j) in self._blocks: blocks_sparse[i][j] = self._blocks[i, j].tosparse(**kwargs) else: m = block_codomain(i).dimension n = block_domain (j).dimension blocks_sparse[i][j] = lil_matrix((m, n)) # Create sparse matrix from sparse blocks M = bmat( blocks_sparse ) M.eliminate_zeros() # Sanity check assert M.shape[0] == self.codomain.dimension assert M.shape[1] == self. domain.dimension return M
# ...
[docs] def toarray(self, **kwargs): """ Convert to Numpy 2D array. """ return self.tosparse(**kwargs).toarray()
# ...
[docs] def dot(self, v, out=None): if self.n_block_cols == 1: assert isinstance(v, Vector) else: assert isinstance(v, BlockVector) assert v.space is self.domain if out is not None: if self.n_block_rows == 1: assert isinstance(out, Vector) else: assert isinstance(out, BlockVector) assert out.space is self.codomain out *= 0.0 else: out = self.codomain.zeros() if not v.ghost_regions_in_sync: v.update_ghost_regions() self._func(self._blocks_as_args, v, out, **self._args) out.ghost_regions_in_sync = False return out
#... @staticmethod def _dot(blocks, v, out, n_rows, n_cols, inc): if n_rows == 1: for (_, j), L0j in blocks.items(): out += L0j.dot(v[j], out=inc) elif n_cols == 1: for (i, _), Li0 in blocks.items(): out[i] += Li0.dot(v, out=inc[i]) else: for (i, j), Lij in blocks.items(): out[i] += Lij.dot(v[j], out=inc[i]) # ...
[docs] def transpose(self, conjugate=False, out=None): """" Return the transposed BlockLinearOperator, or the Hermitian Transpose if conjugate==True Parameters ---------- conjugate : Bool(optional) True to get the Hermitian adjoint. out : BlockLinearOperator(optional) Optional out for the transpose to avoid temporaries """ if out is not None: assert isinstance(out, BlockLinearOperator) assert out.codomain is self.domain assert out.domain is self.codomain for (i, j), Lij in self._blocks.items(): if out[j,i]==None: out[j, i] = Lij.transpose(conjugate=conjugate) else: Lij.transpose(conjugate=conjugate, out=out[j,i]) else: blocks, blocks_T = self.compute_interface_matrices_transpose() blocks = {(j, i): b.transpose(conjugate=conjugate) for (i, j), b in blocks.items()} blocks.update(blocks_T) out = BlockLinearOperator(self.codomain, self.domain, blocks=blocks) out.set_backend(self._backend) return out
#-------------------------------------- # Overridden properties/methods #-------------------------------------- def __neg__(self): blocks = {ij: -Bij for ij, Bij in self._blocks.items()} mat = BlockLinearOperator(self.domain, self.codomain, blocks=blocks) if self._backend is not None: mat._func = self._func mat._args = self._args mat._blocks_as_args = [mat._blocks[key]._data for key in self._blocks] mat._backend = self._backend return mat # ... def __mul__(self, a): blocks = {ij: Bij * a for ij, Bij in self._blocks.items()} mat = BlockLinearOperator(self.domain, self.codomain, blocks=blocks) if self._backend is not None: mat._func = self._func mat._args = self._args mat._blocks_as_args = [mat._blocks[key]._data for key in self._blocks] mat._backend = self._backend return mat # ... def __add__(self, M): if not isinstance(M, BlockLinearOperator): return LinearOperator.__add__(self, M) assert M. domain is self.domain assert M.codomain is self.codomain blocks = {} for ij in set(self._blocks.keys()) | set(M._blocks.keys()): Bij = self[ij] Mij = M[ij] if Bij is None: blocks[ij] = Mij.copy() elif Mij is None: blocks[ij] = Bij.copy() else : blocks[ij] = Bij + Mij mat = BlockLinearOperator(self.domain, self.codomain, blocks=blocks) if len(mat._blocks) != len(self._blocks): mat.set_backend(self._backend) elif self._backend is not None: mat._func = self._func mat._args = self._args mat._blocks_as_args = [mat._blocks[key]._data for key in self._blocks] mat._backend = self._backend return mat # ... def __sub__(self, M): if not isinstance(M, BlockLinearOperator): return LinearOperator.__sub__(self, M) assert M. domain is self. domain assert M.codomain is self.codomain blocks = {} for ij in set(self._blocks.keys()) | set(M._blocks.keys()): Bij = self[ij] Mij = M[ij] if Bij is None: blocks[ij] = -Mij elif Mij is None: blocks[ij] = Bij.copy() else : blocks[ij] = Bij - Mij mat = BlockLinearOperator(self.domain, self.codomain, blocks=blocks) if len(mat._blocks) != len(self._blocks): mat.set_backend(self._backend) elif self._backend is not None: mat._func = self._func mat._args = self._args mat._blocks_as_args = [mat._blocks[key]._data for key in self._blocks] mat._backend = self._backend return mat #-------------------------------------- # New properties/methods #--------------------------------------
[docs] def diagonal(self, *, inverse = False, sqrt = 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). Can be combined with sqrt to get the inverse square root. sqrt : bool If True, get the square root of the diagonal. (Default: False). Can be combined with inverse to get the inverse square root. 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, sqrt = sqrt, out = out[i, i]) return out
# ... @property def blocks(self): """ Immutable 2D view (tuple of tuples) of the linear operator, including the empty blocks as 'None' objects. """ return tuple( tuple(self._blocks.get((i, j), None) for j in range(self.n_block_cols)) for i in range(self.n_block_rows)) # ... @property def n_block_rows(self): return self._nrows # ... @property def n_block_cols(self): return self._ncols @property def nonzero_block_indices(self): """ Tuple of (i, j) pairs which identify the non-zero blocks: i is the row index, j is the column index. """ return tuple(self._blocks) # ...
[docs] def update_ghost_regions(self): for Lij in self._blocks.values(): Lij.update_ghost_regions()
# ...
[docs] def exchange_assembly_data(self): for Lij in self._blocks.values(): Lij.exchange_assembly_data()
# ...
[docs] def remove_spurious_entries(self ): for Lij in self._blocks.values(): Lij.remove_spurious_entries()
@property def ghost_regions_in_sync(self): return self._sync @ghost_regions_in_sync.setter def ghost_regions_in_sync( self, value ): assert isinstance( value, bool ) self._sync = value for Lij in self._blocks.values(): Lij.ghost_regions_in_sync = value # ... def __getitem__(self, key): assert isinstance( key, tuple ) assert len( key ) == 2 assert 0 <= key[0] < self.n_block_rows assert 0 <= key[1] < self.n_block_cols return self._blocks.get( key, None ) # ... def __setitem__(self, key, value): assert isinstance( key, tuple ) assert len( key ) == 2 assert 0 <= key[0] < self.n_block_rows assert 0 <= key[1] < self.n_block_cols if value is None: self._blocks.pop( key, None ) return i,j = key assert isinstance( value, LinearOperator ) # Check domain of rhs if self.n_block_cols == 1: assert value.domain is self.domain else: assert value.domain is self.domain[j] # Check codomain of rhs if self.n_block_rows == 1: assert value.codomain is self.codomain else: assert value.codomain is self.codomain[i] self._blocks[i,j] = value # ...
[docs] def transform(self, operation): """ Applies an operation on each block in this BlockLinearOperator. Parameters ---------- operation : LinearOperator -> LinearOperator The operation which transforms each block. """ blocks = {ij: operation(Bij) for ij, Bij in self._blocks.items()} return BlockLinearOperator(self.domain, self.codomain, blocks=blocks)
# ...
[docs] def backend(self): return self._backend
# ...
[docs] def copy(self, out=None): """ Create a copy of self, that can potentially be stored in a given BlockLinearOperator. Parameters ---------- out : BlockLinearOperator(optional) The existing BlockLinearOperator in which we want to copy self. Returns ------- BlockLinearOperator The copy of `self`, either stored in the given BlockLinearOperator `out` (if provided) or in a new one. In the corner case where `out=self` the `self` object is immediately returned. """ if out is not None: if out is self: return self assert isinstance(out, BlockLinearOperator) assert out.domain is self.domain assert out.codomain is self.codomain else: out = BlockLinearOperator(self.domain, self.codomain) for (i, j), Lij in self._blocks.items(): if out[i, j] is None: out[i, j] = Lij.copy() else: Lij.copy(out = out[i, j]) out.set_backend(self._backend) return out
# ... def __imul__(self, a): for Bij in self._blocks.values(): Bij *= a return self # ... def __iadd__(self, M): if not isinstance(M, BlockLinearOperator): return LinearOperator.__add__(self, M) assert M. domain is self. domain assert M.codomain is self.codomain for ij in set(self._blocks.keys()) | set(M._blocks.keys()): Mij = M[ij] if Mij is None: continue Bij = self[ij] if Bij is None: self[ij] = Mij.copy() else: Bij += Mij return self # ... def __isub__(self, M): if not isinstance(M, BlockLinearOperator): return LinearOperator.__sub__(self, M) assert M. domain is self. domain assert M.codomain is self.codomain for ij in set(self._blocks.keys()) | set(M._blocks.keys()): Mij = M[ij] if Mij is None: continue Bij = self[ij] if Bij is None: self[ij] = -Mij else: Bij -= Mij return self # ...
[docs] def topetsc(self): """ Convert to petsc data structure. """ from psydac.linalg.topetsc import mat_topetsc mat = mat_topetsc( self ) return mat
[docs] def compute_interface_matrices_transpose(self): blocks = self._blocks.copy() blocks_T = {} if not self.codomain.parallel: return blocks, blocks_T from psydac.ddm.mpi import mpi as MPI from psydac.linalg.stencil import StencilInterfaceMatrix if not isinstance(self.codomain, BlockVectorSpace): return blocks, blocks_T V = self.codomain for i,j in V.connectivity: ((axis_i,ext_i), (axis_j,ext_j)) = V.connectivity[i,j] Vi = V.spaces[i] Vj = V.spaces[j] if isinstance(Vi, BlockVectorSpace) and isinstance(Vj, BlockVectorSpace): # case of a system of equations block_ij_exists = False blocks_T[j,i] = BlockLinearOperator(Vi, Vj) block_ij = blocks.get((i,j))._blocks.copy() if self[i,j] else None for k1,Vik1 in enumerate(Vi.spaces): for k2,Vjk2 in enumerate(Vj.spaces): cart_i = Vik1.cart cart_j = Vjk2.cart if cart_i.is_comm_null and cart_j.is_comm_null:break if not cart_i.is_comm_null and not cart_j.is_comm_null:break if not (axis_i, ext_i) in Vik1.interfaces: break cart_ij = Vik1.interfaces[axis_i, ext_i].cart assert isinstance(cart_ij, InterfaceCartDecomposition) if not cart_i.is_comm_null: if cart_ij.intercomm.rank == 0: root = MPI.ROOT else: root = MPI.PROC_NULL else: root = 0 if not block_ij_exists: block_ij_exists = self[i,j] is not None block_ij_exists = cart_ij.intercomm.bcast(block_ij_exists, root= root) or block_ij_exists if not block_ij_exists:break blocks.pop((i,j), None) block_ij_k1k2 = block_ij is not None and (k1,k2) in block_ij is not None block_ij_k1k2 = cart_ij.intercomm.bcast(block_ij_k1k2, root= root) or block_ij_k1k2 if block_ij_k1k2: if not cart_i.is_comm_null: block_ij_k1k2 = block_ij.pop((k1,k2)) info = (block_ij_k1k2.domain_start, block_ij_k1k2.codomain_start, block_ij_k1k2.flip, block_ij_k1k2.pads) cart_ij.intercomm.bcast(info, root= root) else: info = cart_ij.intercomm.bcast(None, root=root) block_ij_k1k2 = StencilInterfaceMatrix(Vjk2, Vik1.interfaces[axis_i, ext_i], info[0], info[1], axis_j, axis_i, ext_j, ext_i, flip=info[2], pads=info[3]) block_ji_k2k1 = StencilInterfaceMatrix(Vik1, Vjk2, info[1], info[0], axis_i, axis_j, ext_i, ext_j, flip=info[2], pads=info[3]) data_exchanger = get_data_exchanger(cart_ij, self.dtype, coeff_shape = block_ij_k1k2._data.shape[block_ij_k1k2._ndim:]) data_exchanger.update_ghost_regions(array_minus=block_ij_k1k2._data) if cart_i.is_comm_null: blocks_T[j,i][k2,k1] = block_ij_k1k2.transpose(out=block_ji_k2k1) else: continue break if (j,i) in blocks_T and len(blocks_T[j,i]._blocks) == 0: blocks_T.pop((j,i)) if (i,j) in blocks and len(blocks[i,j]._blocks) == 0: blocks.pop((i,j)) block_ji_exists = False blocks_T[i,j] = BlockLinearOperator(Vj, Vi) block_ji = blocks.get((j,i))._blocks.copy() if self[j,i] else None for k1,Vik1 in enumerate(Vi.spaces): for k2,Vjk2 in enumerate(Vj.spaces): cart_i = Vik1.cart cart_j = Vjk2.cart if cart_i.is_comm_null and cart_j.is_comm_null:break if not cart_i.is_comm_null and not cart_j.is_comm_null:break if not (axis_i, ext_i) in Vik1.interfaces: break interface_cart_i = Vik1.interfaces[axis_i, ext_i].cart interface_cart_j = Vjk2.interfaces[axis_j, ext_j].cart assert isinstance(interface_cart_i, InterfaceCartDecomposition) assert isinstance(interface_cart_j, InterfaceCartDecomposition) if not cart_j.is_comm_null: if interface_cart_i.intercomm.rank == 0: root = MPI.ROOT else: root = MPI.PROC_NULL else: root = 0 if not block_ji_exists: block_ji_exists = self[j,i] is not None block_ji_exists = interface_cart_i.intercomm.bcast(block_ji_exists, root= root) or block_ji_exists if not block_ji_exists:break blocks.pop((j,i), None) block_ji_k2k1 = block_ji is not None and (k2,k1) in block_ji is not None block_ji_k2k1 = interface_cart_i.intercomm.bcast(block_ji_k2k1, root= root) or block_ji_k2k1 if block_ji_k2k1: if not cart_j.is_comm_null: block_ji_k2k1 = block_ji.pop((k2,k1)) info = (block_ji_k2k1.domain_start, block_ji_k2k1.codomain_start, block_ji_k2k1.flip, block_ji_k2k1.pads) interface_cart_i.intercomm.bcast(info, root= root) else: info = interface_cart_i.intercomm.bcast(None, root=root) block_ji_k2k1 = StencilInterfaceMatrix(Vik1, Vjk2.interfaces[axis_j, ext_j], info[0], info[1], axis_i, axis_j, ext_i, ext_j, flip=info[2], pads=info[3]) block_ij_k1k2 = StencilInterfaceMatrix(Vjk2, Vik1, info[1], info[0], axis_j, axis_i, ext_j, ext_i, flip=info[2], pads=info[3]) interface_cart_i.comm.Barrier() data_exchanger = get_data_exchanger(interface_cart_j, self.dtype, coeff_shape = block_ji_k2k1._data.shape[block_ji_k2k1._ndim:]) data_exchanger.update_ghost_regions(array_plus=block_ji_k2k1._data) if cart_j.is_comm_null: blocks_T[i,j][k1,k2] = block_ji_k2k1.transpose(out=block_ij_k1k2) else: continue break if (i,j) in blocks_T and len(blocks_T[i,j]._blocks) == 0: blocks_T.pop((i,j)) if (j,i) in blocks and len(blocks[j,i]._blocks) == 0: blocks.pop((j,i)) elif not isinstance(Vi, BlockVectorSpace) and not isinstance(Vj, BlockVectorSpace): # case of scalar equations cart_i = Vi.cart cart_j = Vj.cart if cart_i.is_comm_null and cart_j.is_comm_null:continue if not cart_i.is_comm_null and not cart_j.is_comm_null:continue if not (axis_i, ext_i) in Vi.interfaces: continue cart_ij = Vi.interfaces[axis_i, ext_i].cart assert isinstance(cart_ij, InterfaceCartDecomposition) if not cart_i.is_comm_null: if cart_ij.intercomm.rank == 0: root = MPI.ROOT else: root = MPI.PROC_NULL else: root = 0 block_ij_exists = self[i,j] is not None block_ij_exists = cart_ij.intercomm.bcast(block_ij_exists, root= root) or block_ij_exists if block_ij_exists: if not cart_i.is_comm_null: block_ij = blocks.pop((i,j)) info = (block_ij.domain_start, block_ij.codomain_start, block_ij.flip, block_ij.pads) cart_ij.intercomm.bcast(info, root= root) else: info = cart_ij.intercomm.bcast(None, root=root) block_ij = StencilInterfaceMatrix(Vj, Vi.interfaces[axis_i, ext_i], info[0], info[1], axis_j, axis_i, ext_j, ext_i, flip=info[2], pads=info[3]) block_ji = StencilInterfaceMatrix(Vi, Vj, info[1], info[0], axis_i, axis_j, ext_i, ext_j, flip=info[2], pads=info[3]) data_exchanger = get_data_exchanger(cart_ij, self.dtype, coeff_shape = block_ij._data.shape[block_ij._ndim:]) data_exchanger.update_ghost_regions(array_minus=block_ij._data) if cart_i.is_comm_null: blocks_T[j,i] = block_ij.transpose(out=block_ji) if not cart_j.is_comm_null: if cart_ij.intercomm.rank == 0: root = MPI.ROOT else: root = MPI.PROC_NULL else: root = 0 block_ji_exists = self[j,i] is not None block_ji_exists = cart_ij.intercomm.bcast(block_ji_exists, root= root) or block_ji_exists if block_ji_exists: if not cart_j.is_comm_null: block_ji = blocks.pop((j,i)) info = (block_ji.domain_start, block_ji.codomain_start, block_ji.flip, block_ji.pads) cart_ij.intercomm.bcast((block_ji.domain_start, block_ji.codomain_start, block_ji.flip, block_ji.pads), root= root) else: info = cart_ij.intercomm.bcast(None, root=root) block_ji = StencilInterfaceMatrix(Vi, Vj.interfaces[axis_j, ext_j], info[0], info[1], axis_i, axis_j, ext_i, ext_j, flip=info[2], pads=info[3]) block_ij = StencilInterfaceMatrix(Vj, Vi, info[1], info[0], axis_j, axis_i, ext_j, ext_i, flip=info[2], pads=info[3]) data_exchanger = get_data_exchanger(cart_ij, self.dtype, coeff_shape = block_ji._data.shape[block_ji._ndim:]) data_exchanger.update_ghost_regions(array_plus=block_ji._data) if cart_j.is_comm_null: blocks_T[i,j] = block_ji.transpose(out=block_ij) return blocks, blocks_T
[docs] def set_backend(self, backend, precompiled=False): if isinstance(self.domain, BlockVectorSpace) and isinstance(self.domain.spaces[0], BlockVectorSpace): return if isinstance(self.codomain, BlockVectorSpace) and isinstance(self.codomain.spaces[0], BlockVectorSpace): return if backend is None:return if backend is self._backend:return raise AttributeError(f'This is the tiny-psydac version - must use precompiled kernels (but {precompiled = })!') from psydac.api.ast.linalg import LinearOperatorDot from psydac.linalg.stencil import StencilInterfaceMatrix, StencilMatrix if not all(isinstance(b, (StencilMatrix, StencilInterfaceMatrix)) for b in self._blocks.values()): for b in self._blocks.values(): b.set_backend(backend) return block_shape = (self.n_block_rows, self.n_block_cols) keys = self.nonzero_block_indices ndim = self._blocks[keys[0]]._ndim c_starts = [] d_starts = [] interface = isinstance(self._blocks[keys[0]], StencilInterfaceMatrix) if interface: interface_axis = self._blocks[keys[0]]._codomain_axis d_ext = self._blocks[keys[0]]._domain_ext d_axis = self._blocks[keys[0]]._domain_axis flip_axis = self._blocks[keys[0]]._flip permutation = self._blocks[keys[0]]._permutation for key in keys: c_starts.append(self._blocks[key]._codomain_start) d_starts.append(self._blocks[key]._domain_start) c_starts = tuple(c_starts) d_starts = tuple(d_starts) else: interface_axis = None flip_axis = (1,)*ndim permutation = None c_starts = None d_starts = None starts = [] nrows = [] nrows_extra = [] gpads = [] pads = [] dm = [] cm = [] for key in keys: nrows.append(self._blocks[key]._dotargs_null['nrows']) nrows_extra.append(self._blocks[key]._dotargs_null['nrows_extra']) gpads.append(self._blocks[key]._dotargs_null['gpads']) pads.append(self._blocks[key]._dotargs_null['pads']) starts.append(self._blocks[key]._dotargs_null['starts']) cm.append(self._blocks[key]._dotargs_null['cm']) dm.append(self._blocks[key]._dotargs_null['dm']) if self.domain.parallel: if interface: comm = self.domain.spaces[0].interfaces[d_axis, d_ext].cart.local_comm if isinstance(self.domain, BlockVectorSpace) else self.domain.interfaces[d_axis, d_ext].cart.local_comm else: comm = self.codomain.spaces[0].cart.comm if isinstance(self.codomain, BlockVectorSpace) else self.codomain.cart.comm if self.domain == self.codomain: # In this case nrows_extra[i] == 0 for all i dot = LinearOperatorDot(ndim, block_shape=block_shape, keys=keys, comm=comm, backend=frozenset(backend.items()), gpads=tuple(gpads), pads=tuple(pads), dm=tuple(dm), cm=tuple(cm), interface=interface, flip_axis=flip_axis, interface_axis=interface_axis, d_start=d_starts, c_start=c_starts, dtype=self._domain.dtype) self._args = {} for k,key in enumerate(keys): key_str = ''.join(str(i) for i in key) starts_k = starts[k] for i in range(len(starts_k)): self._args['s{}_{}'.format(key_str, i+1)] = np.int64(starts_k[i]) for k,key in enumerate(keys): key_str = ''.join(str(i) for i in key) nrows_k = nrows[k] for i in range(len(nrows_k)): self._args['n{}_{}'.format(key_str, i+1)] = np.int64(nrows_k[i]) for k,key in enumerate(keys): key_str = ''.join(str(i) for i in key) nrows_extra_k = nrows_extra[k] for i in range(len(nrows_extra_k)): self._args['ne{}_{}'.format(key_str, i+1)] = np.int64(nrows_extra_k[i]) else: dot = LinearOperatorDot(ndim, block_shape=block_shape, keys=keys, comm=comm, backend=frozenset(backend.items()), gpads=tuple(gpads), pads=tuple(pads), dm=tuple(dm), cm=tuple(cm), interface=interface, flip_axis=flip_axis, interface_axis=interface_axis, d_start=d_starts, c_start=c_starts, dtype=self._domain.dtype) self._args = {} for k,key in enumerate(keys): key_str = ''.join(str(i) for i in key) starts_k = starts[k] for i in range(len(starts_k)): self._args['s{}_{}'.format(key_str, i+1)] = np.int64(starts_k[i]) for k,key in enumerate(keys): key_str = ''.join(str(i) for i in key) nrows_k = nrows[k] for i in range(len(nrows_k)): self._args['n{}_{}'.format(key_str, i+1)] = np.int64(nrows_k[i]) for k,key in enumerate(keys): key_str = ''.join(str(i) for i in key) nrows_extra_k = nrows_extra[k] for i in range(len(nrows_extra_k)): self._args['ne{}_{}'.format(key_str, i+1)] = np.int64(nrows_extra_k[i]) else: dot = LinearOperatorDot(ndim, block_shape=block_shape, keys=keys, comm=None, backend=frozenset(backend.items()), starts=tuple(starts), nrows=tuple(nrows), nrows_extra=tuple(nrows_extra), gpads=tuple(gpads), pads=tuple(pads), dm=tuple(dm), cm=tuple(cm), interface=interface, flip_axis=flip_axis, interface_axis=interface_axis, d_start=d_starts, c_start=c_starts, dtype=self._domain.dtype) self._args = {} self._blocks_as_args = [self._blocks[key]._data for key in keys] dot = dot.func if interface: def func(blocks, v, out, **args): vs = [vi._interface_data[d_axis, d_ext] for vi in v.blocks] if isinstance(v, BlockVector) else [v._data] outs = [outi._data for outi in out.blocks] if isinstance(out, BlockVector) else [out._data] dot(*blocks, *vs, *outs, **args) else: def func(blocks, v, out, **args): vs = [vi._data for vi in v.blocks] if isinstance(v, BlockVector) else [v._data] outs = [outi._data for outi in out.blocks] if isinstance(out, BlockVector) else [out._data] dot(*blocks, *vs, *outs, **args) self._func = func self._backend = backend