diff --git a/docs/source/modules/feec.multipatch.rst b/docs/source/modules/feec.multipatch.rst index 64bf49a12..85bf08af1 100644 --- a/docs/source/modules/feec.multipatch.rst +++ b/docs/source/modules/feec.multipatch.rst @@ -7,11 +7,6 @@ feec.multipatch :toctree: STUBDIR :template: autosummary/module.rst - multipatch.api - multipatch.fem_linear_operators multipatch.multipatch_domain_utilities - multipatch.non_matching_operators - multipatch.operators - multipatch.plotting_utilities multipatch.utilities multipatch.utils_conga_2d diff --git a/docs/source/modules/feec.rst b/docs/source/modules/feec.rst index 5d424f396..f75dfb8cc 100644 --- a/docs/source/modules/feec.rst +++ b/docs/source/modules/feec.rst @@ -7,8 +7,10 @@ feec :toctree: STUBDIR :template: autosummary/module.rst + feec.conforming_projectors feec.derivatives - feec.global_projectors + feec.global_geometric_projectors + feec.hodge feec.pull_push feec.pushforward diff --git a/docs/source/modules/linalg.rst b/docs/source/modules/linalg.rst index 7ccbbf491..a625296ef 100644 --- a/docs/source/modules/linalg.rst +++ b/docs/source/modules/linalg.rst @@ -14,6 +14,7 @@ linalg linalg.kernels linalg.kron linalg.solvers + linalg.sparse linalg.stencil linalg.topetsc linalg.utilities diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 1cea9397e..bedec470e 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -30,7 +30,7 @@ from psydac.api.fem import DiscreteBilinearForm from psydac.api.fem import DiscreteLinearForm from psydac.api.fem import DiscreteFunctional -from psydac.api.feec import DiscreteDerham +from psydac.api.feec import DiscreteDeRham, DiscreteDeRhamMultipatch from psydac.api.glt import DiscreteGltExpr from psydac.api.expr import DiscreteExpr from psydac.api.equation import DiscreteEquation @@ -47,6 +47,7 @@ __all__ = ( 'discretize', 'discretize_derham', + 'discretize_derham_multipatch', 'reduce_space_degrees', 'discretize_space', 'discretize_domain' @@ -147,10 +148,10 @@ def get_max_degree(*spaces): #============================================================================== def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): """ - Create a discrete De Rham sequence from a symbolic one. + Create a discrete de Rham sequence from a symbolic one. This function creates the discrete spaces from the symbolic ones, and then - creates a DiscreteDerham object from them. + creates a DiscreteDeRham object from them. Parameters ---------- @@ -168,18 +169,16 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): Returns ------- - DiscreteDerham - The discrete De Rham sequence containing the discrete spaces, + DiscreteDeRham + The discrete de Rham sequence containing the discrete spaces, differential operators and projectors. See Also -------- discretize_space - """ ldim = derham.shape - mapping = domain_h.domain.mapping # NOTE: assuming single-patch domain! bases = ['B'] + ldim * ['M'] spaces = [discretize_space(V, domain_h, basis=basis, **kwargs) for V, basis in zip(derham.spaces, bases)] @@ -192,7 +191,48 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): #We still need to specify the symbolic space because of "_recursive_element_of" not implemented in sympde spaces.append(Xh) - return DiscreteDerham(mapping, *spaces) + return DiscreteDeRham(domain_h, *spaces) + +#============================================================================== +def discretize_derham_multipatch(derham, domain_h, **kwargs): + """ + Create a discrete multipatch de Rham sequence from a symbolic one. + + This function creates the broken discrete spaces from the symbolic ones, and then + creates a DiscreteDeRhamMultipatch object from them. + + Parameters + ---------- + derham : sympde.topology.space.Derham + The symbolic Derham sequence. + + domain_h : Geometry + Discrete domain where the spaces will be discretized. + + **kwargs : dict + Optional parameters for the space discretization. + + Returns + ------- + DiscreteDeRhamMultipatch + The discrete multipatch de Rham sequence containing the discrete spaces, + differential operators and projectors. + + See Also + -------- + discretize_derham + discretize_space + """ + + ldim = derham.shape + bases = ['B'] + ldim * ['M'] + spaces = [discretize_space(V, domain_h, basis=basis, **kwargs) \ + for V, basis in zip(derham.spaces, bases)] + + return DiscreteDeRhamMultipatch( + domain_h = domain_h, + spaces = spaces + ) #============================================================================== def reduce_space_degrees(V, Vh, *, basis='B', sequence='DR'): @@ -631,9 +671,12 @@ def discretize(a, *args, **kwargs): elif isinstance(a, BasicFunctionSpace): return discretize_space(a, *args, **kwargs) - elif isinstance(a, Derham): + elif isinstance(a, Derham) and not a.V0.is_broken: return discretize_derham(a, *args, **kwargs) + elif isinstance(a, Derham) and a.V0.is_broken: + return discretize_derham_multipatch(a, *args, **kwargs) + elif isinstance(a, Domain): return discretize_domain(a, *args, **kwargs) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index cfec097eb..2bd95ffbd 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -1,45 +1,54 @@ -from sympde.topology.mapping import Mapping - -from psydac.api.basic import BasicDiscrete -from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D -from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D -from psydac.feec.derivatives import Divergence_2D, Divergence_3D -from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_H1vec -from psydac.feec.global_projectors import Projector_Hdiv, Projector_L2 -from psydac.feec.pull_push import pull_1d_h1, pull_1d_l2 -from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_hdiv, pull_2d_l2, pull_2d_h1vec -from psydac.feec.pull_push import pull_3d_h1, pull_3d_hcurl, pull_3d_hdiv, pull_3d_l2, pull_3d_h1vec -from psydac.fem.basic import FemSpace -from psydac.fem.vector import VectorFemSpace - -__all__ = ('DiscreteDerham',) +from psydac.api.basic import BasicDiscrete + +from psydac.feec.derivatives import Derivative1D, Gradient2D, Gradient3D +from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D +from psydac.feec.derivatives import Divergence2D, Divergence3D +from psydac.feec.derivatives import BrokenGradient2D +from psydac.feec.derivatives import BrokenScalarCurl2D + +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHcurl +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1vec +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHdiv +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorL2 +from psydac.feec.global_geometric_projectors import MultipatchGeometricProjector + +from psydac.feec.conforming_projectors import ConformingProjectionV0 +from psydac.feec.conforming_projectors import ConformingProjectionV1 + +from psydac.feec.hodge import HodgeOperator + +from psydac.feec.pull_push import pull_1d_h1, pull_1d_l2 +from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl +from psydac.feec.pull_push import pull_2d_hdiv, pull_2d_l2, pull_2d_h1vec +from psydac.feec.pull_push import pull_3d_h1, pull_3d_hcurl +from psydac.feec.pull_push import pull_3d_hdiv, pull_3d_l2, pull_3d_h1vec + +from psydac.fem.basic import FemSpace, FemLinearOperator +from psydac.fem.vector import VectorFemSpace +from psydac.linalg.basic import IdentityOperator + +__all__ = ('DiscreteDeRham', 'DiscreteDeRhamMultipatch',) #============================================================================== -class DiscreteDerham(BasicDiscrete): +class DiscreteDeRham(BasicDiscrete): """ A discrete de Rham sequence built over a single-patch geometry. Parameters ---------- - mapping : Mapping or None - Symbolic mapping from the logical space to the physical space, if any. + domain_h : Geometry + The discretized domain, which is a single-patch geometry. *spaces : list of FemSpace The discrete spaces of the de Rham sequence. Notes ----- - - The basic type Mapping is defined in module sympde.topology.mapping. - A discrete mapping (spline or NURBS) may be attached to it. - - This constructor should not be called directly, but rather from the `discretize_derham` function in `psydac.api.discretization`. - - - For the multipatch counterpart of this class please see - `MultipatchDiscreteDerham` in `psydac.feec.multipatch.api`. """ - def __init__(self, mapping, *spaces): + def __init__(self, domain_h, *spaces): - assert (mapping is None) or isinstance(mapping, Mapping) assert all(isinstance(space, FemSpace) for space in spaces) self.has_vec = isinstance(spaces[-1], VectorFemSpace) @@ -53,51 +62,75 @@ def __init__(self, mapping, *spaces): dim = len(spaces) - 1 self._spaces = spaces + self._domain_h = domain_h + self._sequence = tuple(space.symbolic_space.kind.name for space in spaces) self._dim = dim - self._mapping = mapping - self._callable_mapping = mapping.get_callable_mapping() if mapping else None + self._mapping = domain_h.domain.mapping + self._callable_mapping = self._mapping.get_callable_mapping() if self._mapping else None if dim == 1: - D0 = Derivative_1D(spaces[0], spaces[1]) + D0 = Derivative1D(spaces[0], spaces[1]) + spaces[0].diff = spaces[0].grad = D0 + self._derivatives = (D0,) + elif dim == 2: kind = spaces[1].symbolic_space.kind.name if kind == 'hcurl': - D0 = Gradient_2D(spaces[0], spaces[1]) - D1 = ScalarCurl_2D(spaces[1], spaces[2]) + D0 = Gradient2D(spaces[0], spaces[1]) + D1 = ScalarCurl2D(spaces[1], spaces[2]) spaces[0].diff = spaces[0].grad = D0 spaces[1].diff = spaces[1].curl = D1 + self._derivatives = (D0, D1) + elif kind == 'hdiv': - D0 = VectorCurl_2D(spaces[0], spaces[1]) - D1 = Divergence_2D(spaces[1], spaces[2]) + D0 = VectorCurl2D(spaces[0], spaces[1]) + D1 = Divergence2D(spaces[1], spaces[2]) spaces[0].diff = spaces[0].rot = D0 spaces[1].diff = spaces[1].div = D1 + self._derivatives = (D0, D1) + + elif dim == 3: - D0 = Gradient_3D(spaces[0], spaces[1]) - D1 = Curl_3D(spaces[1], spaces[2]) - D2 = Divergence_3D(spaces[2], spaces[3]) + D0 = Gradient3D(spaces[0], spaces[1]) + D1 = Curl3D(spaces[1], spaces[2]) + D2 = Divergence3D(spaces[2], spaces[3]) spaces[0].diff = spaces[0].grad = D0 spaces[1].diff = spaces[1].curl = D1 spaces[2].diff = spaces[2].div = D2 + self._derivatives = (D0, D1, D2) + else: raise ValueError('Dimension {} is not available'.format(dim)) + self._hodge_operators = () + self._conf_proj = () #-------------------------------------------------------------------------- @property def dim(self): """Dimension of the physical and logical domains, which are assumed to be the same.""" return self._dim + + @property + def domain_h(self): + """Discretized domain.""" + return self._domain_h + + @property + def spaces(self): + """Spaces of the proper de Rham sequence (excluding Hvec).""" + return self._spaces @property def V0(self): @@ -124,6 +157,10 @@ def V3(self): """Fourth space of the de Rham sequence : L2 space in 3d""" return self._spaces[3] + @property + def sequence(self): + return self._sequence + @property def H1vec(self): """Vector-valued H1 space built as the Cartesian product of N copies of V0, @@ -131,11 +168,6 @@ def H1vec(self): assert self.has_vec return self._H1vec - @property - def spaces(self): - """Spaces of the proper de Rham sequence (excluding Hvec).""" - return self._spaces - @property def mapping(self): """The mapping from the logical space to the physical space.""" @@ -146,32 +178,17 @@ def callable_mapping(self): """The mapping as a callable.""" return self._callable_mapping - @property - def derivatives_as_matrices(self): - """Differential operators of the De Rham sequence as LinearOperator objects.""" - return tuple(V.diff.matrix for V in self.spaces[:-1]) - - @property - def derivatives(self): - """Differential operators of the De Rham sequence as `DiffOperator` objects. - - Those are objects with `domain` and `codomain` properties that are `FemSpace`, - they act on `FemField` (they take a `FemField` of their `domain` as input and return - a `FemField` of their `codomain`. - """ - return tuple(V.diff for V in self.spaces[:-1]) - #-------------------------------------------------------------------------- def projectors(self, *, kind='global', nquads=None): """Projectors mapping callable functions of the physical coordinates to a - corresponding `FemField` object in the De Rham sequence. + corresponding `FemField` object in the de Rham sequence. Parameters ---------- kind : str Type of the projection : at the moment, only global is accepted and returns geometric commuting projectors based on interpolation/histopolation - for the De Rham sequence (GlobalProjector objects). + for the de Rham sequence (GlobalProjector objects). nquads : list(int) | tuple(int) Number of quadrature points along each direction, to be used in Gauss @@ -182,7 +199,7 @@ def projectors(self, *, kind='global', nquads=None): P0, ..., Pn : callables Projectors that can be called on any callable function that maps from the physical space to R (scalar case) or R^d (vector case) and - returns a FemField belonging to the i-th space of the De Rham sequence + returns a FemField belonging to the i-th space of the de Rham sequence """ if not (kind == 'global'): @@ -200,8 +217,8 @@ def projectors(self, *, kind='global', nquads=None): assert all(nq >= 1 for nq in nquads) if self.dim == 1: - P0 = Projector_H1(self.V0) - P1 = Projector_L2(self.V1, nquads) + P0 = GlobalGeometricProjectorH1(self.V0) + P1 = GlobalGeometricProjectorL2(self.V1, nquads) if self.mapping: P0_m = lambda f: P0(pull_1d_h1(f, self.callable_mapping)) P1_m = lambda f: P1(pull_1d_l2(f, self.callable_mapping)) @@ -209,19 +226,19 @@ def projectors(self, *, kind='global', nquads=None): return P0, P1 elif self.dim == 2: - P0 = Projector_H1(self.V0) - P2 = Projector_L2(self.V2, nquads) + P0 = GlobalGeometricProjectorH1(self.V0) + P2 = GlobalGeometricProjectorL2(self.V2, nquads) kind = self.V1.symbolic_space.kind.name if kind == 'hcurl': - P1 = Projector_Hcurl(self.V1, nquads) + P1 = GlobalGeometricProjectorHcurl(self.V1, nquads) elif kind == 'hdiv': - P1 = Projector_Hdiv(self.V1, nquads) + P1 = GlobalGeometricProjectorHdiv(self.V1, nquads) else: raise TypeError('projector of space type {} is not available'.format(kind)) if self.has_vec : - Pvec = Projector_H1vec(self.H1vec, nquads) + Pvec = GlobalGeometricProjectorH1vec(self.H1vec, nquads) if self.mapping: P0_m = lambda f: P0(pull_2d_h1(f, self.callable_mapping)) @@ -242,12 +259,12 @@ def projectors(self, *, kind='global', nquads=None): return P0, P1, P2 elif self.dim == 3: - P0 = Projector_H1 (self.V0) - P1 = Projector_Hcurl(self.V1, nquads) - P2 = Projector_Hdiv (self.V2, nquads) - P3 = Projector_L2 (self.V3, nquads) + P0 = GlobalGeometricProjectorH1 (self.V0) + P1 = GlobalGeometricProjectorHcurl(self.V1, nquads) + P2 = GlobalGeometricProjectorHdiv (self.V2, nquads) + P3 = GlobalGeometricProjectorL2 (self.V3, nquads) if self.has_vec : - Pvec = Projector_H1vec(self.H1vec) + Pvec = GlobalGeometricProjectorH1vec(self.H1vec) if self.mapping: P0_m = lambda f: P0(pull_3d_h1 (f, self.callable_mapping)) P1_m = lambda f: P1(pull_3d_hcurl(f, self.callable_mapping)) @@ -264,3 +281,328 @@ def projectors(self, *, kind='global', nquads=None): else : return P0, P1, P2, P3 + #-------------------------------------------------------------------------- + def derivatives(self, kind='femlinop'): + if kind == 'femlinop': + return self._derivatives + elif kind == 'linop': + return tuple(b_diff.linop for b_diff in self._derivatives) + + #-------------------------------------------------------------------------- + def conforming_projectors(self, kind='femlinop', mom_pres=False, p_moments=-1, hom_bc=False): + """ + return the conforming projectors of the broken multi-patch space + + Parameters + ---------- + + p_moments : + The number of moments preserved by the projector. + + hom_bc: + Apply homogenous boundary conditions if True + + kind : + The kind of the projector, can be 'femlinop' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'linop' returns a psydac LinearOperator + + Returns + ------- + cP0, cP1, cP2 : Tuple of or + The conforming projectors of each space and in desired form. + + """ + + if hom_bc is None: + raise ValueError('please provide a value for "hom_bc" argument') + + if self.dim == 1: + raise NotImplementedError("1D projectors are not available") + + elif self.dim == 2: + if self.sequence[1] != 'hcurl': + raise NotImplementedError('2D sequence with H-div not available yet') + + else: + + if not self._conf_proj: + + cP0 = ConformingProjectionV0(self.V0, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) + cP1 = ConformingProjectionV1(self.V1, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) + + I2 = IdentityOperator(self.V2.coeff_space) + cP2 = FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=I2) + + self._conf_proj = (cP0, cP1, cP2) + + if kind == 'femlinop': + return self._conf_proj[0], self._conf_proj[1], self._conf_proj[2] + elif kind == 'linop': + return self._conf_proj[0].linop, self._conf_proj[1].linop, self._conf_proj[2].linop + + elif self.dim == 3: + raise NotImplementedError("3D projectors are not available") + + #-------------------------------------------------------------------------- + def _init_hodge_operators(self, backend_language='python'): + """ + Initialize the Hodge operator for the multipatch de Rham sequence. + + Parameters + ---------- + + backend_language: + The backend used to accelerate the code + + """ + if not self._hodge_operators: + + if self.dim == 1: + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language) + + self._hodge_operators = (H0, H1) + + elif self.dim == 2: + + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language) + H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language) + + self._hodge_operators = (H0, H1, H2) + + elif self.dim == 3: + + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language) + H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language) + H3 = HodgeOperator(self.V3, self.domain_h, backend_language=backend_language) + + self._hodge_operators = (H0, H1, H2, H3) + + #-------------------------------------------------------------------------- + def _get_hodge_operator(self, H, dual=False, kind='femlinop'): + """ + Helper function to return the Hodge operator in the specified form. + + Parameters + ---------- + H : + + dual : + If True, returns the dual Hodge operator + + kind : + The kind of the projector, can be 'femlinop' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'linop' returns a psydac LinearOperator + + Returns + ------- + Hodge operator in the specified form. + """ + + if not dual: + if kind == 'femlinop': + return H.hodge + elif kind == 'linop': + return H.linop + else: + if kind == 'femlinop': + return H.dual_hodge + elif kind == 'linop': + return H.dual_linop + + #-------------------------------------------------------------------------- + def hodge_operator(self, space=None, dual=False, kind='femlinop', backend_language='python'): + """ + Returns the Hodge operator for the given space and specified kind. + + Parameters + ---------- + space : str or None + The space for which to return the Hodge operator, can be 'V0', 'V1', 'V2' or None. + If None, returns a tuple with all three Hodge operators. + + dual : bool + If True, returns the dual Hodge operator. + + kind : + The kind of the projector, can be 'femlinop' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'linop' returns a psydac LinearOperator + + backend_language : str + The backend used to accelerate the code, default is 'python'. + + Returns + ------- + The Hodge operator of the space of the specified kind. + + H : or + """ + + if not self._hodge_operators: + self._init_hodge_operators(backend_language=backend_language) + + if space == 'V0': + return self._get_hodge_operator(self._hodge_operators[0], dual=dual, kind=kind) + + elif space == 'V1': + return self._get_hodge_operator(self._hodge_operators[1], dual=dual, kind=kind) + + elif space == 'V2': + return self._get_hodge_operator(self._hodge_operators[2], dual=dual, kind=kind) + + elif space == 'V3': + return self._get_hodge_operator(self._hodge_operators[3], dual=dual, kind=kind) + + #-------------------------------------------------------------------------- + def hodge_operators(self, dual=False, kind='femlinop', backend_language='python'): + """ + Returns the Hodge operators for the specified kind. + + Parameters + ---------- + dual : bool + If True, returns the dual Hodge operator. + + kind : + The kind of the projector, can be 'femlinop' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'linop' returns a psydac LinearOperator + + backend_language : str + The backend used to accelerate the code, default is 'python'. + + Returns + ------- + The Hodge operators of all spaces and of the specified kind. + """ + + if not self._hodge_operators: + self._init_hodge_operators(backend_language=backend_language) + + return tuple(self._get_hodge_operator(H, dual=dual, kind=kind) for H in self._hodge_operators) + + +#============================================================================== +class DiscreteDeRhamMultipatch(DiscreteDeRham): + """ Represents the discrete de Rham sequence for multipatch domains. + It only works when the number of patches>1. + + Parameters + ---------- + domain_h: + The discrete domain + + spaces: + The discrete spaces that are contained in the de Rham sequence + """ + + def __init__(self, *, domain_h, spaces): + + dim = len(spaces) - 1 + self._spaces = tuple(spaces) + self._dim = dim + self._mapping = domain_h.domain.mapping + self._callable_mapping = [m.get_callable_mapping() for m in self._mapping.mappings.values()] if self._mapping else None + self._domain_h = domain_h + self._sequence = tuple(space.symbolic_space.kind.name for space in spaces) + + + if dim == 1: + raise NotImplementedError('1D FEEC multipatch non available yet') + + elif dim == 2: + + if self._sequence[1] == 'hcurl': + + self._derivatives = ( + BrokenGradient2D(self.V0, self.V1), + BrokenScalarCurl2D(self.V1, self.V2), # None, + ) + + elif self._sequence[1] == 'hdiv': + raise NotImplementedError('2D sequence with H-div not available yet') + + else: + raise ValueError('2D sequence not understood') + + elif dim == 3: + raise NotImplementedError('3D FEEC multipatch non available yet') + + else: + raise ValueError('Dimension {} is not available'.format(dim)) + + self._hodge_operators = () + self._conf_proj = () + + #-------------------------------------------------------------------------- + @property + def H1vec(self): + raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') + + #-------------------------------------------------------------------------- + def projectors(self, *, kind='global', nquads=None): + """ + This method returns the patch-wise commuting projectors on the broken multi-patch space + + Parameters + ---------- + kind: + The projectors kind, can be global or local + + nquads: + The number of quadrature points. + + Returns + ------- + P0: + Patch wise H1 projector + + P1: + Patch wise Hcurl projector + + P2: + Patch wise L2 projector + + Notes + ----- + - when applied to smooth functions they return conforming fields + - default 'global projectors' correspond to geometric interpolation/histopolation operators on Greville grids + - here 'global' is a patch-level notion, as the interpolation-type problems are solved on each patch independently + """ + if not (kind == 'global'): + raise NotImplementedError('only global projectors are available') + + if self.dim == 1: + raise NotImplementedError("1D projectors are not available") + + elif self.dim == 2: + P0 = MultipatchGeometricProjector(self.V0, GlobalGeometricProjectorH1) + + if self.sequence[1] == 'hcurl': + P1 = MultipatchGeometricProjector(self.V1, GlobalGeometricProjectorHcurl, nquads=nquads) + else: + P1 = MultipatchGeometricProjector(self.V1, GlobalGeometricProjectorHdiv, nquads=nquads) + + P2 = MultipatchGeometricProjector(self.V2, GlobalGeometricProjectorL2, nquads=nquads) + + if self.mapping: + P0_m = lambda f : P0([pull_2d_h1(f, m) for m in self.callable_mapping]) + + if self.sequence[1] == 'hcurl': + P1_m = lambda f : P1([pull_2d_hcurl(f, m) for m in self.callable_mapping]) + else: + raise NotImplementedError('2D sequence with H-div not available yet') + + P2_m = lambda f : P2([pull_2d_l2(f, m) for m in self.callable_mapping]) + + return P0_m, P1_m, P2_m + + return P0, P1, P2 + + elif self.dim == 3: + raise NotImplementedError("3D projectors are not available") diff --git a/psydac/api/tests/test_api_2d_fields.py b/psydac/api/tests/test_api_2d_fields.py index abd7a1d75..d829bcfd5 100644 --- a/psydac/api/tests/test_api_2d_fields.py +++ b/psydac/api/tests/test_api_2d_fields.py @@ -32,10 +32,10 @@ from sympde.expr import Norm from sympde.expr import find, EssentialBC -from psydac.fem.basic import FemField -from psydac.api.discretization import discretize -from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL -from psydac.feec.global_projectors import Projector_H1 +from psydac.fem.basic import FemField +from psydac.api.discretization import discretize +from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 # ... get the mesh directory try: @@ -142,7 +142,7 @@ def run_boundary_field_test(domain, boundary, f, ncells): x,y = domain.coordinates f_lambda = lambdify([x,y], f, 'math') - Pi0 = Projector_H1(Vh) + Pi0 = GlobalGeometricProjectorH1(Vh) fh = Pi0(f_lambda) fh.coeffs.update_ghost_regions() diff --git a/psydac/api/tests/test_api_feec_1d.py b/psydac/api/tests/test_api_feec_1d.py index ad60a2c02..fd7e7f7dd 100644 --- a/psydac/api/tests/test_api_feec_1d.py +++ b/psydac/api/tests/test_api_feec_1d.py @@ -145,7 +145,7 @@ class CollelaMapping1D(Mapping): M1 = a1_h.assemble() # Differential operators - D0, = derham_h.derivatives_as_matrices + D0, = derham_h.derivatives(kind='linop') # Transpose of derivative matrix D0_T = D0.T diff --git a/psydac/api/tests/test_api_feec_2d.py b/psydac/api/tests/test_api_feec_2d.py index 40b607615..c221af521 100644 --- a/psydac/api/tests/test_api_feec_2d.py +++ b/psydac/api/tests/test_api_feec_2d.py @@ -325,7 +325,7 @@ class CollelaMapping2D(Mapping): M2 = a2_h.assemble() # Differential operators (StencilMatrix or BlockLinearOperator objects) - D0, D1 = derham_h.derivatives_as_matrices + D0, D1 = derham_h.derivatives(kind='linop') # Discretize and assemble penalization matrix if not periodic: diff --git a/psydac/api/tests/test_api_feec_3d.py b/psydac/api/tests/test_api_feec_3d.py index 9da51f784..3a05bdf0b 100644 --- a/psydac/api/tests/test_api_feec_3d.py +++ b/psydac/api/tests/test_api_feec_3d.py @@ -130,8 +130,8 @@ def run_maxwell_3d_scipy(logical_domain, mapping, e_ex, b_ex, ncells, degree, pe M1 = a1_h.assemble().tosparse().tocsc() M2 = a2_h.assemble().tosparse().tocsr() - # Get differential operators as BlockLinearOperator objects - GRAD, CURL, DIV = derham_h.derivatives_as_matrices + # Diff operators + GRAD, CURL, DIV = derham_h.derivatives(kind='linop') # Get projectors as objects of type Projector_H1, Projector_Hcurl, Projector_Hdiv, Projector_L2 P0, P1, P2, P3 = derham_h.projectors(nquads=[5, 5, 5]) @@ -233,8 +233,8 @@ def run_maxwell_3d_stencil(logical_domain, mapping, e_ex, b_ex, ncells, degree, M1 = a1_h.assemble() M2 = a2_h.assemble() - # Get differential operators as BlockLinearOperator objects - GRAD, CURL, DIV = derham_h.derivatives_as_matrices + # Diff operators + GRAD, CURL, DIV = derham_h.derivatives(kind='linop') # Get projectors as objects of type Projector_H1, Projector_Hcurl, Projector_Hdiv, Projector_L2 P0, P1, P2, P3 = derham_h.projectors(nquads=[5, 5, 5]) diff --git a/psydac/api/tests/test_assembly.py b/psydac/api/tests/test_assembly.py index a84a41a2b..189b6c102 100644 --- a/psydac/api/tests/test_assembly.py +++ b/psydac/api/tests/test_assembly.py @@ -544,7 +544,7 @@ def test_assembly_no_synchr_args(backend): V1h = derham_h.V1 #differential operator - div, = derham_h.derivatives_as_matrices + div, = derham_h.derivatives(kind='linop') rho = element_of(V1h.symbolic_space, name='rho') g = element_of(V1h.symbolic_space, name='g') diff --git a/psydac/feec/multipatch/non_matching_operators.py b/psydac/feec/conforming_projectors.py similarity index 68% rename from psydac/feec/multipatch/non_matching_operators.py rename to psydac/feec/conforming_projectors.py index 4b172e7de..d045a702e 100644 --- a/psydac/feec/multipatch/non_matching_operators.py +++ b/psydac/feec/conforming_projectors.py @@ -1,21 +1,24 @@ -""" -This module provides utilities for constructing the conforming projections -for a H1-Hcurl-L2 broken FEEC de Rham sequence. -""" - +# coding: utf-8 +# Conga operators on piecewise (broken) de Rham sequences import os - import numpy as np -from scipy.sparse import eye as sparse_eye -from scipy.sparse import csr_matrix +from scipy.sparse import eye as sparse_eye +from scipy.sparse import csr_matrix +from scipy.special import comb from sympde.topology import Boundary, Interface -from psydac.fem.splines import SplineSpace -from psydac.utilities.quadratures import gauss_legendre -from psydac.core.bsplines import quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid +from psydac.core.bsplines import quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid +from psydac.fem.basic import FemLinearOperator +from psydac.fem.splines import SplineSpace +from psydac.utilities.quadratures import gauss_legendre +from psydac.linalg.sparse import SparseMatrixLinearOperator +__all__ = ( + 'ConformingProjectionV0', + 'ConformingProjectionV1', +) def get_patch_index_from_face(domain, face): """ @@ -118,7 +121,6 @@ def get_corners(domain, boundary_only): patches = domain.interior.args bd = domain.boundary - # corner_data[corner] = (patch_ind => local coordinates) corner_data = dict() if boundary_only: @@ -143,7 +145,6 @@ def get_corners(domain, boundary_only): else: for co in cos: corner_data[co] = dict() - for cb in co.corners: p_ind = patches.index(cb.domain) c_coord = cb.coordinates @@ -192,63 +193,48 @@ def construct_restriction_operator_1D( """ n_c = coarse_space_1d.nbasis n_f = fine_space_1d.nbasis - R = np.zeros((n_c, n_f)) if coarse_space_1d.basis == 'B': + #map V^+ to V^+_0 T = np.zeros((n_f, n_f)) - for i in range(1, n_f - 1): + for i in range(n_f): for j in range(n_f): - T[i, j] = int(i == j) - E[i, 0] * int(0 == j) - \ - E[i, -1] * int(n_f - 1 == j) + T[i, j] = int(i == j) - E[i, 0] * int(0 == j) - E[i, -1] * int(n_f - 1 == j) - cf_mass_mat = calculate_mixed_mass_matrix(coarse_space_1d, fine_space_1d)[ - 1:-1, 1:-1].transpose() - c_mass_mat = calculate_mass_matrix(coarse_space_1d)[1:-1, 1:-1] + cf_mass_mat = calculate_mixed_mass_matrix(coarse_space_1d, fine_space_1d).transpose() + c_mass_mat = calculate_mass_matrix(coarse_space_1d) if p_moments > 0: - - if not p_moments % 2 == 0: - p_moments += 1 - c_poly_mat = calculate_poly_basis_integral( - coarse_space_1d, p_moments=p_moments - 1)[:, 1:-1] - f_poly_mat = calculate_poly_basis_integral( - fine_space_1d, p_moments=p_moments - 1)[:, 1:-1] - - c_mass_mat[0:p_moments // 2, :] = c_poly_mat[0:p_moments // 2, :] - c_mass_mat[-p_moments // 2:, :] = c_poly_mat[-p_moments // 2:, :] - - cf_mass_mat[0:p_moments // 2, :] = f_poly_mat[0:p_moments // 2, :] - cf_mass_mat[-p_moments // 2:, :] = f_poly_mat[-p_moments // 2:, :] - - R0 = np.linalg.solve(c_mass_mat, cf_mass_mat) - R[1:-1, 1:-1] = R0 - R = R @ T - + # L^2 projection from V^+_0 to V^- + R[:, 1:-1] = np.linalg.solve(c_mass_mat, cf_mass_mat[:, 1:-1]) + gamma = get_1d_moment_correction(coarse_space_1d, p_moments=p_moments) + n = len(gamma) + + # maps V^- to V^+_0 in a moment preserving way + T2 = np.eye(n_c) + T2[0, 0] = T2[-1, -1] = 0 + T2[1:n+1, 0] += gamma + T2[-(n+1):-1, -1] += gamma[::-1] + + # maps V^+ to V^- in a moment preserving way + R = T2 @ R @ T + + else: + R[1:-1, 1:-1] = np.linalg.solve(c_mass_mat[1:-1, 1:-1], cf_mass_mat[1:-1, 1:-1]) + R = R @ T + + # add the degrees of freedom of T back R[0, 0] += 1 R[-1, -1] += 1 + else: - cf_mass_mat = calculate_mixed_mass_matrix( - coarse_space_1d, fine_space_1d).transpose() + cf_mass_mat = calculate_mixed_mass_matrix(coarse_space_1d, fine_space_1d).transpose() c_mass_mat = calculate_mass_matrix(coarse_space_1d) - if p_moments > 0: - - if not p_moments % 2 == 0: - p_moments += 1 - c_poly_mat = calculate_poly_basis_integral( - coarse_space_1d, p_moments=p_moments - 1) - f_poly_mat = calculate_poly_basis_integral( - fine_space_1d, p_moments=p_moments - 1) - - c_mass_mat[0:p_moments // 2, :] = c_poly_mat[0:p_moments // 2, :] - c_mass_mat[-p_moments // 2:, :] = c_poly_mat[-p_moments // 2:, :] - - cf_mass_mat[0:p_moments // 2, :] = f_poly_mat[0:p_moments // 2, :] - cf_mass_mat[-p_moments // 2:, :] = f_poly_mat[-p_moments // 2:, :] - + # The pure L^2 projection is already moment preserving R = np.linalg.solve(c_mass_mat, cf_mass_mat) return R @@ -287,8 +273,7 @@ def get_extension_restriction(coarse_space_1d, fine_space_1d, p_moments=-1): spl_type = coarse_space_1d.basis if not matching_interfaces: - grid = np.linspace( - fine_space_1d.breaks[0], fine_space_1d.breaks[-1], coarse_space_1d.ncells + 1) + grid = np.linspace(fine_space_1d.breaks[0], fine_space_1d.breaks[-1], coarse_space_1d.ncells + 1) coarse_space_1d_k_plus = SplineSpace( degree=fine_space_1d.degree, grid=grid, @@ -297,25 +282,17 @@ def get_extension_restriction(coarse_space_1d, fine_space_1d, p_moments=-1): E_1D = construct_extension_operator_1D( domain=coarse_space_1d_k_plus, codomain=fine_space_1d) + R_1D = construct_restriction_operator_1D( coarse_space_1d_k_plus, fine_space_1d, E_1D, p_moments) - ER_1D = E_1D @ R_1D + assert np.allclose(R_1D @ E_1D, np.eye(coarse_space_1d.nbasis), 1e-12, 1e-12) + else: ER_1D = R_1D = E_1D = sparse_eye( fine_space_1d.nbasis, format="lil") - # TODO remove later - assert ( - np.allclose( - np.linalg.norm( - R_1D @ E_1D - - np.eye( - coarse_space_1d.nbasis)), - 0, - 1e-12, - 1e-12)) return E_1D, R_1D, ER_1D @@ -418,8 +395,7 @@ def calculate_mixed_mass_matrix(domain_space, codomain_space): fine_basis = basis_ders_on_quad_grid(fknots, fdeg, quad_x, 0, spl_type) coarse_basis = [ basis_ders_on_irregular_grid( - knots, deg, q, cell_index( - breaks, q), 0, spl_type) for q in quad_x] + knots, deg, q, cell_index(breaks, q), 0, spl_type) for q in quad_x] fine_spans = elements_spans(fknots, deg) coarse_spans = [find_spans(knots, deg, q[0])[0] for q in quad_x] @@ -471,7 +447,6 @@ def calculate_poly_basis_integral(space_1d, p_moments=-1): enddom = breaks[-1] begdom = breaks[0] denom = enddom - begdom - order = max(p_moments + 1, deg + 1) u, w = gauss_legendre(order) @@ -484,8 +459,7 @@ def calculate_poly_basis_integral(space_1d, p_moments=-1): Mass_mat = np.zeros((p_moments + 1, space_1d.nbasis)) for ie1 in range(Nel): # loop on cells - for pol in range( - p_moments + 1): # loops on basis function in each cell + for pol in range(p_moments + 1): # loops on basis function in each cell for il2 in range(deg + 1): # loops on basis function in each cell val = 0. @@ -494,7 +468,7 @@ def calculate_poly_basis_integral(space_1d, p_moments=-1): x = quad_x[ie1, q1] # val += quad_w[ie1, q1] * v0 * ((enddom-x)/denom)**pol val += quad_w[ie1, q1] * v0 * \ - ((enddom - x) / denom)**(p_moments - pol) * (x / denom)**pol + comb(p_moments, pol) * ((enddom - x) / denom)**(p_moments - pol) * ((x - begdom) / denom)**pol locind2 = il2 + spans[ie1] - deg Mass_mat[pol, locind2] += val @@ -520,27 +494,24 @@ def get_1d_moment_correction(space_1d, p_moments=-1): """ if p_moments < 0: - return None + return [] if space_1d.ncells <= p_moments + 1: - print("Careful, the correction term is currently not independent of the mesh.") - + p_moments = space_1d.ncells - 2 + print(f"The prescribed degree of preserved moments was too high, given the number of cells in the patch. It has been reduced to degree {p_moments}.") + if p_moments >= 0: # to preserve moments of degree p we need 1+p conforming basis functions in the patch (the "interior" ones) # and for the given regularity constraint, there are # local_shape[conf_axis]-2*(1+reg) such conforming functions p_max = space_1d.nbasis - 3 if p_max < p_moments: - print( - " ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") + print(" ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") print(" ** WARNING -- WARNING -- WARNING ") - print( - f" ** conf. projection imposing C0 smoothness on scalar space along this axis :") - print( - f" ** there are not enough dofs in a patch to preserve moments of degree {p_moments} !") + print(f" ** conf. projection imposing C0 smoothness on scalar space along this axis :") + print(f" ** there are not enough dofs in a patch to preserve moments of degree {p_moments} !") print(f" ** Only able to preserve up to degree --> {p_max} <-- ") - print( - " ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") + print(" ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") p_moments = p_max Mass_mat = calculate_poly_basis_integral(space_1d, p_moments) @@ -549,14 +520,16 @@ def get_1d_moment_correction(space_1d, p_moments=-1): return gamma -def construct_h1_conforming_projection( - Vh, reg_orders=0, p_moments=-1, hom_bc=False): +#============================================================================== +# Multipatch conforming projectors +#============================================================================== +def construct_h1_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=False): """ Construct the conforming projection for a scalar space for a given regularity (0 continuous, -1 discontinuous). Parameters ---------- - Vh : TensorFemSpace + Vh : MultipatchFemSpace Finite Element Space coming from the discrete de Rham sequence. reg_orders : (int) @@ -582,8 +555,8 @@ def construct_h1_conforming_projection( # moment corrections perpendicular to interfaces # assume same moments everywhere - gamma = get_1d_moment_correction( - Vh.patch_spaces[0].spaces[0], p_moments=p_moments) + gamma = get_1d_moment_correction(Vh.spaces[0].spaces[0], p_moments=p_moments) + p_moments = len(gamma)-1 domain = Vh.symbolic_space.domain ndim = 2 @@ -592,22 +565,21 @@ def construct_h1_conforming_projection( l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) for k in range(n_patches): - Vk = Vh.patch_spaces[k] + Vk = Vh.spaces[k] # T is a TensorFemSpace and S is a 1D SplineSpace shapes = [S.nbasis for S in Vk.spaces] l2g.set_patch_shapes(k, shapes) # P vertex # vertex correction matrix - Proj_vertex = sparse_eye(dim_tot, format="lil") + Proj_vertex = sparse_eye(dim_tot, format="lil") corner_indices = set() corners = get_corners(domain, False) def get_vertex_index_from_patch(patch, coords): - # coords = co[patch] - nbasis0 = Vh.patch_spaces[patch].spaces[coords[0]].nbasis - 1 - nbasis1 = Vh.patch_spaces[patch].spaces[coords[1]].nbasis - 1 + nbasis0 = Vh.spaces[patch].spaces[coords[0]].nbasis - 1 + nbasis1 = Vh.spaces[patch].spaces[coords[1]].nbasis - 1 # patch local index multi_index = [None] * ndim @@ -621,12 +593,11 @@ def vertex_moment_indices(axis, coords, patch, p_moments): if coords[axis] == 0: return range(1, p_moments + 2) else: - return range(Vh.patch_spaces[patch].spaces[coords[axis]].nbasis - 1 - 1, - Vh.patch_spaces[patch].spaces[coords[axis]].nbasis - 1 - p_moments - 2, -1) + return range(Vh.spaces[patch].spaces[coords[axis]].nbasis - 1 - 1, + Vh.spaces[patch].spaces[coords[axis]].nbasis - 1 - p_moments - 2, -1) # loop over all vertices for (bd, co) in corners.items(): - # len(co)=#v is the number of adjacent patches at a vertex corr = len(co) @@ -639,9 +610,9 @@ def vertex_moment_indices(axis, coords, patch, p_moments): corner_indices.add(ig) for patch2 in co: - # local vertex coordinates in patch2 coords2 = co[patch2] + # global index jg = get_vertex_index_from_patch(patch2, coords2) @@ -701,7 +672,6 @@ def vertex_moment_indices(axis, coords, patch, p_moments): corners = get_corners(domain, True) if hom_bc: for (bd, co) in corners.items(): - for patch1 in co: # local vertex coordinates in patch2 @@ -714,6 +684,7 @@ def vertex_moment_indices(axis, coords, patch, p_moments): # local vertex coordinates in patch2 coords2 = co[patch2] + # global index jg = get_vertex_index_from_patch(patch2, coords2) @@ -830,8 +801,8 @@ def get_mu_minus(j, coarse_space, fine_space, R): k_minus = get_patch_index_from_face(domain, I.minus) k_plus = get_patch_index_from_face(domain, I.plus) - I_minus_ncells = Vh.patch_spaces[k_minus].ncells - I_plus_ncells = Vh.patch_spaces[k_plus].ncells + I_minus_ncells = Vh.spaces[k_minus].ncells + I_plus_ncells = Vh.spaces[k_plus].ncells # logical directions normal to interface if I_minus_ncells <= I_plus_ncells: @@ -848,8 +819,8 @@ def get_mu_minus(j, coarse_space, fine_space, R): d_fine = 1 - fine_axis d_coarse = 1 - coarse_axis - space_fine = Vh.patch_spaces[k_fine] - space_coarse = Vh.patch_spaces[k_coarse] + space_fine = Vh.spaces[k_fine] + space_coarse = Vh.spaces[k_coarse] coarse_space_1d = space_coarse.spaces[d_coarse] fine_space_1d = space_fine.spaces[d_fine] @@ -962,7 +933,7 @@ def get_mu_minus(j, coarse_space, fine_space, R): if hom_bc: for bn in domain.boundary: k = get_patch_index_from_face(domain, bn) - space_k = Vh.patch_spaces[k] + space_k = Vh.spaces[k] axis = bn.axis d = 1 - axis @@ -979,17 +950,19 @@ def get_mu_minus(j, coarse_space, fine_space, R): pg = edge_moment_index(p, i, axis, ext, space_k, k) Proj_edge[pg, ig] = gamma[p] else: - if corner_indices.issuperset({ig}): - mu_minus = get_mu_minus( - j, space_k_1d, space_k_1d, np.eye( - space_k_1d.nbasis)) + #if corner_indices.issuperset({ig}): + mu_minus = get_mu_minus( + i, space_k_1d, space_k_1d, np.eye( + space_k_1d.nbasis)) - for p in range(p_moments + 1): - for m in range(space_k_1d.nbasis): - pg = edge_moment_index( - p, m, axis, ext, space_k, k) - Proj_edge[pg, ig] = gamma[p] * mu_minus[m] - else: + for p in range(p_moments + 1): + for m in range(space_k_1d.nbasis): + pg = edge_moment_index( + p, m, axis, ext, space_k, k) + Proj_edge[pg, ig] = gamma[p] * mu_minus[m] + + if not corner_indices.issuperset({ig}): + corner_indices.add(ig) multi_index = [None] * ndim for p in range(p_moments + 1): @@ -997,22 +970,20 @@ def get_mu_minus(j, coarse_space, fine_space, R): 1 else space_k.spaces[axis].nbasis - 1 - p - 1 for pd in range(p_moments + 1): multi_index[1 - axis] = pd + \ - 1 if i == 0 else space_k.spaces[1 - - axis].nbasis - 1 - pd - 1 + 1 if i == 0 else space_k.spaces[1 - axis].nbasis - 1 - pd - 1 pg = l2g.get_index(k, 0, multi_index) Proj_edge[pg, ig] = gamma[p] * gamma[pd] return Proj_edge @ Proj_vertex -def construct_hcurl_conforming_projection( - Vh, reg_orders=0, p_moments=-1, hom_bc=False): +def construct_hcurl_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=False): """ Construct the conforming projection for a vector Hcurl space for a given regularity (0 continuous, -1 discontinuous). Parameters ---------- - Vh : TensorFemSpace + Vh : MultipatchFemSpace Finite Element Space coming from the discrete de Rham sequence. reg_orders : (int) @@ -1037,8 +1008,9 @@ def construct_hcurl_conforming_projection( return sparse_eye(dim_tot, format="lil") # moment corrections perpendicular to interfaces - gamma = [get_1d_moment_correction( - Vh.patch_spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] + # should be in the V^0 spaces + gamma = [get_1d_moment_correction(Vh.spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] + p_moments = min([len(g) for g in gamma])-1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1047,7 +1019,7 @@ def construct_hcurl_conforming_projection( l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) for k in range(n_patches): - Vk = Vh.patch_spaces[k] + Vk = Vh.spaces[k] # T is a TensorFemSpace and S is a 1D SplineSpace shapes = [[S.nbasis for S in T.spaces] for T in Vk.spaces] l2g.set_patch_shapes(k, *shapes) @@ -1073,7 +1045,7 @@ def edge_moment_index(p, i, axis, ext, space, k): multi_index[axis] = p + 1 if ext == - \ 1 else space.spaces[1 - axis].spaces[axis].nbasis - 1 - p - 1 return l2g.get_index(k, 1 - axis, multi_index) - + # loop over all interfaces for I in Interfaces: direction = I.ornt @@ -1086,8 +1058,8 @@ def edge_moment_index(p, i, axis, ext, space, k): minus_axis, plus_axis = I.minus.axis, I.plus.axis # logical directions along the interface d_minus, d_plus = 1 - minus_axis, 1 - plus_axis - I_minus_ncells = Vh.patch_spaces[k_minus].spaces[d_minus].ncells[d_minus] - I_plus_ncells = Vh.patch_spaces[k_plus].spaces[d_plus].ncells[d_plus] + I_minus_ncells = Vh.spaces[k_minus].spaces[d_minus].ncells[d_minus] + I_plus_ncells = Vh.spaces[k_plus].spaces[d_plus].ncells[d_plus] # logical directions normal to interface if I_minus_ncells <= I_plus_ncells: @@ -1104,8 +1076,8 @@ def edge_moment_index(p, i, axis, ext, space, k): d_fine = 1 - fine_axis d_coarse = 1 - coarse_axis - space_fine = Vh.patch_spaces[k_fine] - space_coarse = Vh.patch_spaces[k_coarse] + space_fine = Vh.spaces[k_fine] + space_coarse = Vh.spaces[k_coarse] coarse_space_1d = space_coarse.spaces[d_coarse].spaces[d_coarse] fine_space_1d = space_fine.spaces[d_fine].spaces[d_fine] @@ -1164,7 +1136,7 @@ def edge_moment_index(p, i, axis, ext, space, k): # boundary condition for bn in domain.boundary: k = get_patch_index_from_face(domain, bn) - space_k = Vh.patch_spaces[k] + space_k = Vh.spaces[k] axis = bn.axis if not hom_bc: @@ -1184,3 +1156,346 @@ def edge_moment_index(p, i, axis, ext, space, k): Proj_edge[pg, ig] = gamma[d][p] return Proj_edge + +#============================================================================== +# Singlepatch conforming projectors +#============================================================================== +def construct_h1_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=False): + """ + Construct the conforming projection for a scalar space for a given regularity (0 continuous, -1 discontinuous). + + Parameters + ---------- + Vh : MultipatchFemSpace + Finite Element Space coming from the discrete de Rham sequence. + + reg_orders : (int) + Regularity in each space direction -1 or 0. + + p_moments : (int) + Number of moments to be preserved. + + hom_bc : (bool) + Homogeneous boundary conditions. + + Returns + ------- + cP : scipy.sparse.csr_array + Conforming projection as a sparse matrix. + """ + + dim_tot = Vh.nbasis + + # fully discontinuous space + if reg_orders < 0 or not hom_bc: + return sparse_eye(dim_tot, format="lil") + + # moment corrections perpendicular to interfaces + # assume same moments everywhere + gamma = get_1d_moment_correction(Vh.spaces[0], p_moments=p_moments) + p_moments = len(gamma)-1 + + domain = Vh.symbolic_space.domain + ndim = 2 + n_components = 1 + n_patches = len(domain) + + l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) + # T is a TensorFemSpace and S is a 1D SplineSpace + shapes = [S.nbasis for S in Vh.spaces] + l2g.set_patch_shapes(0, shapes) + + # P vertex + # vertex correction matrix + Proj_vertex = sparse_eye(dim_tot, format="lil") + + + def get_vertex_index(coords): + nbasis0 = Vh.spaces[coords[0]].nbasis - 1 + nbasis1 = Vh.spaces[coords[1]].nbasis - 1 + + # patch local index + multi_index = [None] * ndim + multi_index[0] = 0 if coords[0] == 0 else nbasis0 + multi_index[1] = 0 if coords[1] == 0 else nbasis1 + + # global index + return l2g.get_index(0, 0, multi_index) + + def vertex_moment_indices(axis, coords, p_moments): + if coords[axis] == 0: + return range(1, p_moments + 2) + else: + return range(Vh.spaces[coords[axis]].nbasis - 1 - 1, + Vh.spaces[coords[axis]].nbasis - 1 - p_moments - 2, -1) + + # boundary conditions + + for co in [(0,0), (1,0), (0,1), (1,1)]: + + # global index + ig = get_vertex_index(co) + + # conformity constraint + Proj_vertex[ig, ig] = 0 + + + if p_moments == -1: + continue + + # moment corrections from patch1 to patch1 + axis = 0 + d = 1 + multi_index_p = [None] * ndim + + d_moment_index = vertex_moment_indices(d, co, p_moments) + axis_moment_index = vertex_moment_indices(axis, co, p_moments) + + for pd in range(0, p_moments + 1): + multi_index_p[d] = d_moment_index[pd] + + for p in range(0, p_moments + 1): + multi_index_p[axis] = axis_moment_index[p] + + pg = l2g.get_index(0, 0, multi_index_p) + Proj_vertex[pg, ig] = gamma[p] * gamma[pd] + + # P edge + # edge correction matrix + Proj_edge = sparse_eye(dim_tot, format="lil") + + def get_edge_index(j, axis, ext): + multi_index = [None] * ndim + multi_index[axis] = 0 if ext == - 1 else Vh.spaces[axis].nbasis - 1 + multi_index[1 - axis] = j + return l2g.get_index(0, 0, multi_index) + + def edge_moment_index(p, i, axis, ext): + multi_index = [None] * ndim + multi_index[1 - axis] = i + multi_index[axis] = p + 1 if ext == -1 else Vh.spaces[axis].nbasis - 1 - p - 1 + return l2g.get_index(0, 0, multi_index) + + + def get_mu_minus(j, coarse_space, fine_space, R): + mu_plus = np.zeros(fine_space.nbasis) + mu_minus = np.zeros(coarse_space.nbasis) + + if j == 0: + mu_minus[0] = 1 + for p in range(p_moments + 1): + mu_plus[p + 1] = gamma[p] + else: + mu_minus[-1] = 1 + for p in range(p_moments + 1): + mu_plus[-1 - (p + 1)] = gamma[p] + + for m in range(coarse_space.nbasis): + for l in range(fine_space.nbasis): + mu_minus[m] += R[m, l] * mu_plus[l] + + if j == 0: + mu_minus[m] -= R[m, 0] + else: + mu_minus[m] -= R[m, -1] + + return mu_minus + + + # boundary condition + for bn in domain.boundary: + space_k = Vh + axis = bn.axis + + d = 1 - axis + ext = bn.ext + space_k_1d = space_k.spaces[d] + + for i in range(0, space_k_1d.nbasis): + ig = get_edge_index(i, axis, ext) + Proj_edge[ig, ig] = 0 + + if (i != 0 and i != space_k_1d.nbasis - 1): + for p in range(p_moments + 1): + + pg = edge_moment_index(p, i, axis, ext) + Proj_edge[pg, ig] = gamma[p] + else: + #if corner_indices.issuperset({ig}): + mu_minus = get_mu_minus( + i, space_k_1d, space_k_1d, np.eye( + space_k_1d.nbasis)) + + for p in range(p_moments + 1): + for m in range(space_k_1d.nbasis): + pg = edge_moment_index( + p, m, axis, ext) + Proj_edge[pg, ig] = gamma[p] * mu_minus[m] + + + return Proj_edge @ Proj_vertex + + +def construct_hcurl_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=False): + """ + Construct the conforming projection for a single patch vector Hcurl space for a given regularity (0 continuous, -1 discontinuous). + + Parameters + ---------- + Vh : MultipatchFemSpace + Finite Element Space coming from the discrete de Rham sequence. + + reg_orders : (int) + Regularity in each space direction -1 or 0. + + p_moments : (int) + Number of polynomial moments to be preserved. + + hom_bc : (bool) + Tangential homogeneous boundary conditions. + + Returns + ------- + cP : scipy.sparse.csr_array + Conforming projection as a sparse matrix. + """ + + dim_tot = Vh.nbasis + + # fully discontinuous space + if reg_orders < 0 or not hom_bc: + return sparse_eye(dim_tot, format="lil") + + # moment corrections perpendicular to interfaces + # should be in the V^0 spaces + + gamma = [get_1d_moment_correction(Vh.spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] + p_moments = min([len(g) for g in gamma])-1 + + domain = Vh.symbolic_space.domain + ndim = 2 + n_components = 2 + n_patches = len(domain) + + l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) + # T is a TensorFemSpace and S is a 1D SplineSpace + shapes = [[S.nbasis for S in T.spaces] for T in Vh.spaces] + l2g.set_patch_shapes(0, *shapes) + + # P edge + # edge correction matrix + Proj_edge = sparse_eye(dim_tot, format="lil") + + def get_edge_index(j, axis, ext): + multi_index = [None] * ndim + multi_index[axis] = 0 if ext == -1 else Vh.spaces[1 - axis].spaces[axis].nbasis - 1 + multi_index[1 - axis] = j + return l2g.get_index(0, 1 - axis, multi_index) + + def edge_moment_index(p, i, axis, ext): + multi_index = [None] * ndim + multi_index[1 - axis] = i + multi_index[axis] = p + 1 if ext == -1 else Vh.spaces[1 - axis].spaces[axis].nbasis - 1 - p - 1 + return l2g.get_index(0, 1 - axis, multi_index) + + + # boundary condition + for bn in domain.boundary: + + axis = bn.axis + d = 1 - axis + ext = bn.ext + space_1d = Vh.spaces[d].spaces[d] + + for i in range(0, space_1d.nbasis): + ig = get_edge_index(i, axis, ext) + Proj_edge[ig, ig] = 0 + + for p in range(p_moments + 1): + + pg = edge_moment_index(p, i, axis, ext) + Proj_edge[pg, ig] = gamma[d][p] + + return Proj_edge + + +# =============================================================================== + +class ConformingProjectionV0(FemLinearOperator): + """ + Conforming projection from global broken V0 space to conforming global V0 space + Defined by averaging of interface (including vertex) dofs + and adding moment correction terms + + Parameters + ---------- + V0h: + The discrete space + + p_moments: + Number of polynomial moments to be preserved in the projection. + + hom_bc : + Apply homogenous boundary conditions if True + """ + def __init__( + self, + V0h, + mom_pres=False, + p_moments=-1, + hom_bc=False): + + if mom_pres: + if V0h.is_multipatch: + p_moments = max(p_moments, max(V0h.degree[0])) + else: + p_moments = max(p_moments, max(V0h.degree)) + + FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V0h) + + if V0h.is_multipatch: + sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + else: + sparse_matrix = construct_h1_singlepatch_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, sparse_matrix.tocsr()) + + +class ConformingProjectionV1(FemLinearOperator): + """ + Conforming projection from global broken V1 space to conforming V1 global space + Defined by averaging of (only) interface dofs + and adding moment correction terms + + Parameters + ---------- + V1h: + The discrete space + + p_moments: + Number of polynomial moments to be preserved in the projection. + + hom_bc : + Apply homogenous boundary conditions if True + """ + def __init__( + self, + V1h, + mom_pres=False, + p_moments=-1, + hom_bc=False): + + if mom_pres: + if V1h.is_multipatch: + p_moments = max(p_moments, max(V1h.spaces[0].degree[0])) + else: + p_moments = max(p_moments, max(V1h.degree[0])) + + FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V1h) + + if V1h.is_multipatch: + sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + else: + sparse_matrix = construct_hcurl_singlepatch_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, sparse_matrix.tocsr()) diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index f7f28ccbd..1fd3de5d2 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -9,38 +9,28 @@ from psydac.fem.vector import VectorFemSpace from psydac.fem.tensor import TensorFemSpace from psydac.linalg.basic import IdentityOperator -from psydac.fem.basic import FemField, FemSpace +from psydac.fem.basic import FemField, FemSpace, FemLinearOperator from psydac.linalg.basic import LinearOperator from psydac.ddm.cart import DomainDecomposition, CartDecomposition __all__ = ( 'DirectionalDerivativeOperator', - 'DiffOperator', - 'Derivative_1D', - 'Gradient_2D', - 'Gradient_3D', - 'ScalarCurl_2D', - 'VectorCurl_2D', - 'Curl_3D', - 'Divergence_2D', - 'Divergence_3D', - 'block_tostencil' + 'Derivative1D', + 'Gradient2D', + 'Gradient3D', + 'ScalarCurl2D', + 'VectorCurl2D', + 'Curl3D', + 'Divergence2D', + 'Divergence3D', + 'BrokenGradient2D', + 'BrokenTransposedGradient2D', + 'BrokenScalarCurl2D', + 'BrokenTransposedScalarCurl2D', ) #==================================================================================================== -def block_tostencil(M): - """ - Convert a BlockLinearOperator that contains KroneckerStencilMatrix objects - to a BlockLinearOperator that contains StencilMatrix objects - """ - blocks = [list(b) for b in M.blocks] - for i1,b in enumerate(blocks): - for i2, mat in enumerate(b): - if mat is None: - continue - blocks[i1][i2] = mat.tostencil() - return BlockLinearOperator(M.domain, M.codomain, blocks=blocks) - +# Singlepatch derivative operators #==================================================================================================== class DirectionalDerivativeOperator(LinearOperator): """ @@ -361,40 +351,7 @@ def copy(self): self._diffdir, negative=self._negative, transposed=self._transposed) #==================================================================================================== -class DiffOperator: - def __init__(self, domain, codomain, matrix): - assert isinstance(domain, FemSpace) - assert isinstance(codomain, FemSpace) - assert isinstance(matrix, LinearOperator) - assert domain.coeff_space is matrix.domain - assert codomain.coeff_space is matrix.codomain - - self._domain = domain - self._codomain = codomain - self._matrix = matrix - - @property - def matrix(self): - return self._matrix - - @property - def domain(self): - return self._domain - - @property - def codomain(self): - return self._codomain - - def __call__(self, u): - assert isinstance(u, FemField) - assert u.space == self.domain - - coeffs = self.matrix.dot(u.coeffs) - - return FemField(self.codomain, coeffs=coeffs) - -#==================================================================================================== -class Derivative_1D(DiffOperator): +class Derivative1D(FemLinearOperator): """ 1D derivative. @@ -415,10 +372,10 @@ def __init__(self, H1, L2): assert H1.degree[0] == L2.degree[0] + 1 # Store data in object - super().__init__(H1, L2, DirectionalDerivativeOperator(H1.coeff_space, L2.coeff_space, 0)) + super().__init__(fem_domain = H1, fem_codomain = L2, linop = DirectionalDerivativeOperator(H1.coeff_space, L2.coeff_space, 0)) #==================================================================================================== -class Gradient_2D(DiffOperator): +class Gradient2D(FemLinearOperator): """ Gradient operator in 2D. @@ -434,7 +391,7 @@ class Gradient_2D(DiffOperator): def __init__(self, H1, Hcurl): assert isinstance( H1, TensorFemSpace); assert H1.ldim == 2 - assert isinstance(Hcurl, VectorFemSpace); assert Hcurl.ldim == 2 + assert isinstance(Hcurl, VectorFemSpace); assert Hcurl.ldim == 2 assert Hcurl.spaces[0].periodic == H1.periodic assert Hcurl.spaces[1].periodic == H1.periodic @@ -454,11 +411,10 @@ def __init__(self, H1, Hcurl): matrix = BlockLinearOperator(H1.coeff_space, Hcurl.coeff_space, blocks=blocks) # Store data in object - super().__init__(H1, Hcurl, matrix) - + super().__init__(fem_domain = H1, fem_codomain = Hcurl, linop = matrix) #==================================================================================================== -class Gradient_3D(DiffOperator): +class Gradient3D(FemLinearOperator): """ Gradient operator in 3D. @@ -497,10 +453,10 @@ def __init__(self, H1, Hcurl): matrix = BlockLinearOperator(H1.coeff_space, Hcurl.coeff_space, blocks=blocks) # Store data in object - super().__init__(H1, Hcurl, matrix) + super().__init__(fem_domain = H1, fem_codomain = Hcurl, linop = matrix) #==================================================================================================== -class ScalarCurl_2D(DiffOperator): +class ScalarCurl2D(FemLinearOperator): """ Scalar curl operator in 2D: computes a scalar field from a vector field. @@ -536,10 +492,10 @@ def __init__(self, Hcurl, L2): matrix = BlockLinearOperator(Hcurl.coeff_space, L2.coeff_space, blocks=blocks) # Store data in object - super().__init__(Hcurl, L2, matrix) + super().__init__(fem_domain = Hcurl, fem_codomain = L2, linop = matrix) #==================================================================================================== -class VectorCurl_2D(DiffOperator): +class VectorCurl2D(FemLinearOperator): """ Vector curl operator in 2D: computes a vector field from a scalar field. This is sometimes called the 'rot' operator. @@ -576,10 +532,10 @@ def __init__(self, H1, Hdiv): matrix = BlockLinearOperator(H1.coeff_space, Hdiv.coeff_space, blocks=blocks) # Store data in object - super().__init__(H1, Hdiv, matrix) + super().__init__(fem_domain = H1, fem_codomain = Hdiv, linop = matrix) #==================================================================================================== -class Curl_3D(DiffOperator): +class Curl3D(FemLinearOperator): """ Curl operator in 3D. @@ -626,10 +582,10 @@ def __init__(self, Hcurl, Hdiv): # ... # Store data in object - super().__init__(Hcurl, Hdiv, matrix) + super().__init__(fem_domain = Hcurl, fem_codomain = Hdiv, linop = matrix) #==================================================================================================== -class Divergence_2D(DiffOperator): +class Divergence2D(FemLinearOperator): """ Divergence operator in 2D. @@ -665,10 +621,10 @@ def __init__(self, Hdiv, L2): matrix = BlockLinearOperator(Hdiv.coeff_space, L2.coeff_space, blocks=blocks) # Store data in object - super().__init__(Hdiv, L2, matrix) + super().__init__(fem_domain = Hdiv, fem_codomain = L2, linop = matrix) #==================================================================================================== -class Divergence_3D(DiffOperator): +class Divergence3D(FemLinearOperator): """ Divergence operator in 3D. @@ -707,4 +663,118 @@ def __init__(self, Hdiv, L2): matrix = BlockLinearOperator(Hdiv.coeff_space, L2.coeff_space, blocks=blocks) # Store data in object - super().__init__(Hdiv, L2, matrix) + super().__init__(fem_domain = Hdiv, fem_codomain = L2, linop = matrix) + +#==================================================================================================== +# 2D Multipatch derivative operators +#==================================================================================================== +class BrokenGradient2D(FemLinearOperator): + """ + Gradient operator in a 2D multipatch domain, + acting independently on each patch. + In general, the resulting field is therefore discontinuous, or "broken". + + Parameters + ---------- + V0h : MultipatchFemSpace + Domain of the gradient operator. + + V1h : MultipatchFemSpace + Codomain of the gradient operator. + """ + def __init__(self, V0h, V1h): + + FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V1h) + + D0s = [Gradient2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] + + self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D0i.linop for i, D0i in enumerate(D0s)}) + + def transpose(self, conjugate=False): + # todo (MCP): define as the dual differential operator + return BrokenTransposedGradient2D(self.fem_domain, self.fem_codomain) + +# ============================================================================== +class BrokenTransposedGradient2D(FemLinearOperator): + """ + Transposed gradient operator in a 2D multipatch domain, + acting independently on each patch. + In general, the resulting field is therefore discontinuous, or "broken". + + Parameters + ---------- + V0h : MultipatchFemSpace + Codomain of the transposed gradient operator. + + V1h : MultipatchFemSpace + Domain of the transposed gradient operator. + """ + def __init__(self, V0h, V1h): + + FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V0h) + + D0s = [Gradient2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] + + self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D0i.linop.T for i, D0i in enumerate(D0s)}) + + def transpose(self, conjugate=False): + # todo (MCP): discard + return BrokenGradient2D(self.fem_codomain, self.fem_domain) + +# ============================================================================== +class BrokenScalarCurl2D(FemLinearOperator): + """ + Scalar curl operator in a 2D multipatch domain, + acting independently on each patch. + In general, the resulting field is therefore discontinuous, or "broken". + + Parameters + ---------- + V1h : MultipatchFemSpace + Domain of the scalar curl operator. + + V2h : MultipatchFemSpace + Codomain of the scalar curl operator. + """ + def __init__(self, V1h, V2h): + + FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V2h) + + D1s = [ScalarCurl2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] + + self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D1i.linop for i, D1i in enumerate(D1s)}) + + def transpose(self, conjugate=False): + return BrokenTransposedScalarCurl2D( + V1h=self.fem_domain, V2h=self.fem_codomain) + + +# ============================================================================== +class BrokenTransposedScalarCurl2D(FemLinearOperator): + """ + Transposed scalar curl operator in a 2D multipatch domain, + acting independently on each patch. + In general, the resulting field is therefore discontinuous, or "broken". + + Parameters + ---------- + V1h : MultipatchFemSpace + Codomain of the transposed scalar curl operator. + + V2h : MultipatchFemSpace + Domain of the transposed scalar curl operator. + """ + def __init__(self, V1h, V2h): + + FemLinearOperator.__init__(self, fem_domain=V2h, fem_codomain=V1h) + + D1s = [ScalarCurl2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] + + self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D1i.linop.T for i, D1i in enumerate(D1s)}) + + def transpose(self, conjugate=False): + return BrokenScalarCurl2D(V1h=self.fem_codomain, V2h=self.fem_domain) diff --git a/psydac/feec/global_projectors.py b/psydac/feec/global_geometric_projectors.py similarity index 94% rename from psydac/feec/global_projectors.py rename to psydac/feec/global_geometric_projectors.py index e5a61a0bb..019c2f441 100644 --- a/psydac/feec/global_projectors.py +++ b/psydac/feec/global_geometric_projectors.py @@ -4,26 +4,27 @@ from psydac.linalg.kron import KroneckerLinearSolver, KroneckerStencilMatrix from psydac.linalg.stencil import StencilMatrix, StencilVectorSpace -from psydac.linalg.block import BlockLinearOperator +from psydac.linalg.block import BlockLinearOperator, BlockVector from psydac.core.bsplines import quadrature_grid from psydac.utilities.quadratures import gauss_legendre from psydac.fem.basic import FemField from psydac.fem.tensor import TensorFemSpace -from psydac.fem.vector import VectorFemSpace +from psydac.fem.vector import VectorFemSpace, MultipatchFemSpace from psydac.ddm.cart import DomainDecomposition, CartDecomposition from psydac.utilities.utils import roll_edges from abc import ABCMeta, abstractmethod -__all__ = ('GlobalProjector', 'Projector_H1', 'Projector_Hcurl', 'Projector_Hdiv', 'Projector_L2', +__all__ = ('GlobalGeometricProjector', 'GlobalGeometricProjectorH1', 'GlobalGeometricProjectorHcurl', 'GlobalGeometricProjectorHdiv', 'GlobalGeometricProjectorL2', + 'MultipatchGeometricProjector', 'evaluate_dofs_1d_0form', 'evaluate_dofs_1d_1form', 'evaluate_dofs_2d_0form', 'evaluate_dofs_2d_1form_hcurl', 'evaluate_dofs_2d_1form_hdiv', 'evaluate_dofs_2d_2form', 'evaluate_dofs_3d_0form', 'evaluate_dofs_3d_1form', 'evaluate_dofs_3d_2form', 'evaluate_dofs_3d_3form') #============================================================================== -class GlobalProjector(metaclass=ABCMeta): +class GlobalGeometricProjector(metaclass=ABCMeta): """ Projects callable functions to some scalar or vector FEM space. @@ -43,7 +44,7 @@ class GlobalProjector(metaclass=ABCMeta): space : VectorFemSpace | TensorFemSpace Some finite element space, codomain of the projection operator. The exact structure where to use histopolation and where interpolation - has to be given by a subclass of the GlobalProjector class. + has to be given by a subclass of the GlobalGeometricProjector class. As of now, it is implicitly assumed for a VectorFemSpace, that for each direction that all spaces with interpolation are the same, and all spaces with histopolation are the same (i.e. yield the same quadrature/interpolation points etc.); so use with care on an arbitrary VectorFemSpace. @@ -389,7 +390,9 @@ def __call__(self, fun): return FemField(self._space, coeffs=coeffs) #============================================================================== -class Projector_H1(GlobalProjector): +# SINGLEPATCH PROJECTORS +#============================================================================== +class GlobalGeometricProjectorH1(GlobalGeometricProjector): """ Projector from H1 to an H1-conforming finite element space (i.e. a finite dimensional subspace of H1) constructed with tensor-product B-splines in 1, @@ -439,7 +442,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class Projector_Hcurl(GlobalProjector): +class GlobalGeometricProjectorHcurl(GlobalGeometricProjector): """ Projector from H(curl) to an H(curl)-conforming finite element space, i.e. a finite dimensional subspace of H(curl), constructed with tensor-product @@ -512,7 +515,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class Projector_Hdiv(GlobalProjector): +class GlobalGeometricProjectorHdiv(GlobalGeometricProjector): """ Projector from H(div) to an H(div)-conforming finite element space, i.e. a finite dimensional subspace of H(div), constructed with tensor-product @@ -589,7 +592,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class Projector_L2(GlobalProjector): +class GlobalGeometricProjectorL2(GlobalGeometricProjector): """ Projector from L2 to an L2-conforming finite element space (i.e. a finite dimensional subspace of L2) constructed with tensor-product M-splines in 1, @@ -648,7 +651,8 @@ def __call__(self, fun): """ return super().__call__(fun) -class Projector_H1vec(GlobalProjector): +#============================================================================== +class GlobalGeometricProjectorH1vec(GlobalGeometricProjector): """ Projector from H1^3 = H1 x H1 x H1 to a conforming finite element space, i.e. a finite dimensional subspace of H1^3, constructed with tensor-product @@ -712,6 +716,42 @@ def __call__(self, fun): """ return super().__call__(fun) +#============================================================================== +# MULTIPATCH PROJECTORS (2D) +#============================================================================== +class MultipatchGeometricProjector: + """ + Global Geometric Projector base class for multipatch domains. + + Parameters + ---------- + space : MultipatchFemSpace + Multipatch finite element space, codomain of the projection operator. + Projector : type[GlobalGeometricProjector] + Class of the projector to instantiate for each patch. + nquads : Iterable[int] + Number of quadrature points per cell along each direction. + This is a parameter passed to the constructor of Projector. + """ + + def __init__(self, space, Projector, nquads=None): + assert isinstance(space, MultipatchFemSpace) + assert isinstance(Projector, type) + assert issubclass(Projector, GlobalGeometricProjector) + + self._Vh = Vh = space + self._Ps = [Projector(V, nquads=nquads) for V in Vh.spaces] + + def __call__(self, funs): + """ + project a list of functions given in the logical domain + """ + us = [P(fun) for P, fun, in zip(self._Ps, funs)] + + u_c = BlockVector(self._Vh.coeff_space, blocks=[uj.coeffs for uj in us]) + + return FemField(self._Vh, coeffs=u_c) + #============================================================================== # 1D DEGREES OF FREEDOM #============================================================================== diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py new file mode 100644 index 000000000..26cb702b4 --- /dev/null +++ b/psydac/feec/hodge.py @@ -0,0 +1,148 @@ +import os +import numpy as np + +from sympde.topology import elements_of +from sympde.topology.space import ScalarFunction +from sympde.calculus import dot +from sympde.expr.expr import BilinearForm +from sympde.expr.expr import integral + +from psydac.api.settings import PSYDAC_BACKENDS + +# =============================================================================== +class HodgeOperator: + """ + Change of basis operator: dual basis -> primal basis + + self._linop: matrix (LinearOperator) of the primal Hodge = this is the mass matrix ! + self.dual_linop: this is the INVERSE mass matrix (LinearOperator) + + Parameters + ---------- + Vh: + The discrete space + + domain_h: + The discrete domain of the projector + + metric : + the metric of the de Rham complex + + backend_language: + The backend used to accelerate the code + + Notes + ----- + We only support the identity metric, this implies that the dual Hodge is the inverse of the primal one. + # todo: allow for non-identity metrics + """ + + def __init__(self, Vh, domain_h, metric='identity', backend_language='python'): + + self._fem_domain = Vh + self._fem_codomain = Vh + + # FemLinearOperators + self._primal_hodge = None + self._dual_hodge = None + + # LinearOperators + self._linop = None + self._dual_linop = None + + self._domain_h = domain_h + self._backend_language = backend_language + + if not (metric == 'identity'): + raise NotImplementedError('only the identity metric is available') + + self._metric = metric + + def assemble_matrix(self): + """ + the Hodge matrix is the patch-wise multi-patch mass matrix + it is not stored by default but assembled on demand + """ + from psydac.api.discretization import discretize + from psydac.fem.basic import FemLinearOperator + + if self._linop is None: + Vh = self._fem_domain + assert Vh == self._fem_codomain + + V = Vh.symbolic_space + domain = V.domain + u, v = elements_of(V, names='u, v') + + if isinstance(u, ScalarFunction): + expr = u * v + else: + expr = dot(u, v) + + a = BilinearForm((u, v), integral(domain, expr)) + ah = discretize(a, self._domain_h, [Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language]) + + self._linop = ah.assemble() # Mass matrix in stencil format + + self._primal_hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop) + + def assemble_dual_matrix(self, solver ='cg', **kwargs): + """ + the dual Hodge matrix is the patch-wise inverse of the multi-patch mass matrix + it is not stored by default but computed on demand, by approximate local (patch-wise) inversion of the mass matrix + """ + from psydac.linalg.solvers import inverse + from psydac.linalg.block import BlockLinearOperator + from psydac.fem.basic import FemLinearOperator + + if self._dual_linop is None: + if not self._linop: + self.assemble_matrix() + + M = self._linop # mass matrix of the (primal) basis + + if self._fem_domain.is_multipatch: + + nrows = M.n_block_rows + ncols = M.n_block_cols + + inv_M_blocks = [list(b) for b in M.blocks] + for i in range(nrows): + Mii = M[i, i] + inv_Mii = inverse(Mii, solver=solver, **kwargs) + inv_M_blocks[i][i] = inv_Mii + + self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) + + else: + inv_M = inverse(M, solver=solver, **kwargs) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) + + @property + def linop(self): + if self._linop is None: + self.assemble_matrix() + + return self._linop + + @property + def dual_linop(self): + if self._dual_linop is None: + self.assemble_dual_matrix() + + return self._dual_linop + + @property + def hodge(self): + if self._linop is None: + self.assemble_matrix() + + return self._primal_hodge + + @property + def dual_hodge(self): + if self._dual_linop is None: + self.assemble_dual_matrix() + + return self._dual_hodge diff --git a/psydac/feec/multipatch/api.py b/psydac/feec/multipatch/api.py deleted file mode 100644 index 566815421..000000000 --- a/psydac/feec/multipatch/api.py +++ /dev/null @@ -1,301 +0,0 @@ -# coding: utf-8 -import os - -from sympde.topology import Derham -from sympde.topology import element_of, elements_of -from sympde.topology.space import ScalarFunction -from sympde.calculus import grad, dot, inner, rot, div -from sympde.calculus import laplace, bracket, convect -from sympde.calculus import jump, avg, Dn, minus, plus -from sympde.expr.expr import LinearForm, BilinearForm, integral - -from psydac.api.settings import PSYDAC_BACKENDS - -from psydac.api.discretization import discretize as discretize_single_patch -from psydac.api.discretization import discretize_space -from psydac.api.discretization import DiscreteDerham -from psydac.feec.multipatch.operators import BrokenGradient_2D -from psydac.feec.multipatch.operators import BrokenScalarCurl_2D -from psydac.feec.multipatch.operators import Multipatch_Projector_H1 -from psydac.feec.multipatch.operators import Multipatch_Projector_Hcurl -from psydac.feec.multipatch.operators import Multipatch_Projector_L2 -from psydac.feec.multipatch.operators import ConformingProjection_V0 -from psydac.feec.multipatch.operators import ConformingProjection_V1 -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator - - -__all__ = ('DiscreteDerhamMultipatch', 'discretize', 'discretize_derham_multipatch') - -#============================================================================== -class DiscreteDerhamMultipatch(DiscreteDerham): - """ Represents the discrete De Rham sequence for multipatch domains. - It only works when the number of patches>1 - - Parameters - ---------- - mapping: - The mapping of the multipatch domain, the multipatch mapping contains the mapping of each patch - - domain_h: - The discrete domain - - spaces: - The discrete spaces that are contained in the De Rham sequence - - sequence: - The space kind of each space in the De Rham sequence - """ - - def __init__(self, *, mapping, domain_h, spaces, sequence=None): - - - dim = len(spaces) - 1 - self._dim = dim - self._mapping = mapping - self._spaces = tuple(spaces) - self._domain_h = domain_h - - if sequence: - if len(sequence) != dim + 1: - raise ValueError('Expected len(sequence) = {}, got {} instead'. - format(dim + 1, len(sequence))) - - if dim == 1: - self._sequence = ('h1', 'l2') - raise NotImplementedError('1D FEEC multipatch non available yet') - - elif dim == 2: - if sequence is None: - raise ValueError('Sequence must be specified in 2D case') - - elif tuple(sequence) == ('h1', 'hcurl', 'l2'): - self._sequence = tuple(sequence) - self._broken_diff_ops = ( - BrokenGradient_2D(self.V0, self.V1), - BrokenScalarCurl_2D(self.V1, self.V2), # None, - ) - - elif tuple(sequence) == ('h1', 'hdiv', 'l2'): - self._sequence = tuple(sequence) - raise NotImplementedError('2D sequence with H-div not available yet') - - else: - raise ValueError('2D sequence not understood') - - elif dim == 3: - self._sequence = ('h1', 'hcurl', 'hdiv', 'l2') - raise NotImplementedError('3D FEEC multipatch non available yet') - - else: - raise ValueError('Dimension {} is not available'.format(dim)) - - #-------------------------------------------------------------------------- - @property - def sequence(self): - return self._sequence - - # ... - @property - def broken_derivatives_as_operators(self): - return self._broken_diff_ops - - # ... - @property - def broken_derivatives_as_matrices(self): - return tuple(b_diff.matrix for b_diff in self._broken_diff_ops) - - #-------------------------------------------------------------------------- - def projectors(self, *, kind='global', nquads=None): - """ - This method returns the patch-wise commuting projectors on the broken multi-patch space - - Parameters - ---------- - kind: - The projectors kind, can be global or local - - nquads: - The number of quadrature points. - - Returns - ------- - P0: - Patch wise H1 projector - - P1: - Patch wise Hcurl projector - - P2: - Patch wise L2 projector - - Notes - ----- - - when applied to smooth functions they return conforming fields - - default 'global projectors' correspond to geometric interpolation/histopolation operators on Greville grids - - here 'global' is a patch-level notion, as the interpolation-type problems are solved on each patch independently - """ - if not (kind == 'global'): - raise NotImplementedError('only global projectors are available') - - if self.dim == 1: - raise NotImplementedError("1D projectors are not available") - - elif self.dim == 2: - P0 = Multipatch_Projector_H1(self.V0) - - if self.sequence[1] == 'hcurl': - P1 = Multipatch_Projector_Hcurl(self.V1, nquads=nquads) - else: - P1 = None # TODO: Multipatch_Projector_Hdiv(self.V1, nquads=nquads) - raise NotImplementedError('2D sequence with H-div not available yet') - - P2 = Multipatch_Projector_L2(self.V2, nquads=nquads) - return P0, P1, P2 - - elif self.dim == 3: - raise NotImplementedError("3D projectors are not available") - - #-------------------------------------------------------------------------- - def conforming_projection(self, space, hom_bc=False, backend_language="python", load_dir=None): - """ - return the conforming projectors of the broken multi-patch space - - Parameters - ---------- - space : - The space of the projector - - hom_bc: - Apply homogenous boundary conditions if True - - backend_language: - The backend used to accelerate the code - - load_dir: - Filename for storage in sparse matrix format - - Returns - ------- - Cp: - The conforming projector - - """ - if hom_bc is None: - raise ValueError('please provide a value for "hom_bc" argument') - - if isinstance(load_dir, str): - if not os.path.exists(load_dir): - os.makedirs(load_dir) - if space == 'V0': - P_name = 'cP0' - elif space == 'V1': - P_name = 'cP1' - elif space == 'V2': - P_name = 'cP2' - else: - raise ValueError(space) - - if hom_bc: - storage_fn = load_dir + '/{}_hom_m.npz'.format(P_name) - else: - storage_fn = load_dir + '/{}_m.npz'.format(P_name) - else: - storage_fn = None - - cP = None - if self.dim == 1: - raise NotImplementedError("1D projectors are not available") - - elif self.dim == 2: - if space == 'V0': - cP = ConformingProjection_V0(self.V0, self._domain_h, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) - elif space == 'V1': - if self.sequence[1] == 'hcurl': - cP = ConformingProjection_V1(self.V1, self._domain_h, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) - else: - raise NotImplementedError('2D sequence with H-div not available yet') - - elif space == 'V2': - cP = IdLinearOperator(self.V2) # no storage needed! - else: - raise ValueError('Invalid value for "space" argument: {}'.format(space)) - - elif self.dim == 3: - raise NotImplementedError("3D projectors are not available") - - return cP - - def get_dual_dofs(self, space, f, backend_language="python", return_format='stencil_array'): - """ - return the dual dofs tilde_sigma_i(f) = < Lambda_i, f >_{L2} i = 1, .. dim(V^k)) of a given function f, as a stencil array or numpy array - - Parameters - ---------- - space : - The space of the dual dofs - - f : - The function used for evaluation - - backend_language: - The backend used to accelerate the code - - return_format: - The format of the dofs, can be 'stencil_array' or 'numpy_array' - - Returns - ------- - tilde_f: - The dual dofs - """ - if space == 'V0': - Vh = self.V0 - elif space == 'V1': - Vh = self.V1 - elif space == 'V2': - Vh = self.V2 - else: - raise NotImplementedError("The space of kind {} is not available".format(space)) - - V = Vh.symbolic_space - v = element_of(V, name='v') - - if isinstance(v, ScalarFunction): - expr = f*v - else: - expr = dot(f,v) - - l = LinearForm(v, integral( V.domain, expr)) - lh = discretize(l, self._domain_h, Vh, backend=PSYDAC_BACKENDS[backend_language]) - tilde_f = lh.assemble() - - if return_format == 'numpy_array': - return tilde_f.toarray() - else: - return tilde_f - -#============================================================================== -def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): - - ldim = derham.shape - mapping = derham.spaces[0].domain.mapping - - bases = ['B'] + ldim * ['M'] - spaces = [discretize_space(V, domain_h, *args, basis=basis, **kwargs) \ - for V, basis in zip(derham.spaces, bases)] - - return DiscreteDerhamMultipatch( - mapping = mapping, - domain_h = domain_h, - spaces = spaces, - sequence = [V.kind.name for V in derham.spaces] - ) - -#============================================================================== -def discretize(expr, *args, **kwargs): - - if isinstance(expr, Derham) and expr.V0.is_broken: - return discretize_derham_multipatch(expr, *args, **kwargs) - - else: - return discretize_single_patch(expr, *args, **kwargs) diff --git a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py index 7b1e33792..aaebd3140 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -12,38 +12,21 @@ V0h --grad-> V1h -—curl-> V2h """ - -from mpi4py import MPI - import os import numpy as np -from collections import OrderedDict - -from sympy import lambdify -from scipy.sparse.linalg import spsolve -from sympde.calculus import dot -from sympde.expr.expr import LinearForm -from sympde.expr.expr import integral, Norm from sympde.topology import Derham -from sympde.topology import element_of -from psydac.api.settings import PSYDAC_BACKENDS -from psydac.feec.multipatch.api import discretize -from psydac.feec.pull_push import pull_2d_h1 -from psydac.feec.multipatch.utils_conga_2d import P0_phys +from psydac.api.discretization import discretize +from psydac.linalg.basic import IdentityOperator +from psydac.linalg.solvers import inverse -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from psydac.feec.multipatch.operators import HodgeOperator from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_h1 -from psydac.feec.multipatch.utilities import time_count -from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection -from psydac.api.postprocessing import OutputManager, PostProcessManager +from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_h1 -from psydac.linalg.utilities import array_to_psydac -from psydac.fem.basic import FemField +from psydac.fem.projectors import get_dual_dofs +from psydac.fem.basic import FemField from psydac.api.postprocessing import OutputManager, PostProcessManager @@ -98,9 +81,6 @@ def solve_h1_source_pbm( print('building the multipatch domain...') domain = build_multipatch_domain(domain_name=domain_name) - mappings = OrderedDict([(P.logical_domain, P.mapping) - for P in domain.interior]) - mappings_list = list(mappings.values()) if isinstance(nc, int): ncells = [nc, nc] @@ -114,102 +94,89 @@ def solve_h1_source_pbm( derham = Derham(domain, ["H1", "Hcurl", "L2"]) derham_h = discretize(derham, domain_h, degree=degree) - # multi-patch (broken) spaces - V0h = derham_h.V0 - V1h = derham_h.V1 - V2h = derham_h.V2 + V0h, V1h, V2h = derham_h.spaces print('dim(V0h) = {}'.format(V0h.nbasis)) print('dim(V1h) = {}'.format(V1h.nbasis)) print('dim(V2h) = {}'.format(V2h.nbasis)) print('broken differential operators...') # broken (patch-wise) differential operators - bD0, bD1 = derham_h.broken_derivatives_as_operators - bD0_m = bD0.to_sparse_matrix() - - print('building the discrete operators:') - print('commuting projection operators...') - nquads = [4 * (d + 1) for d in degree] - P0, P1, P2 = derham_h.projectors(nquads=nquads) - - I0 = IdLinearOperator(V0h) - I0_m = I0.to_sparse_matrix() + bD0, bD1 = derham_h.derivatives(kind='linop') print('Hodge operators...') - # multi-patch (broken) linear operators / matrices - H0 = HodgeOperator(V0h, domain_h, backend_language=backend_language) - H1 = HodgeOperator(V1h, domain_h, backend_language=backend_language) - - H0_m = H0.to_sparse_matrix() # = mass matrix of V0 - dH0_m = H0.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V0 - H1_m = H1.to_sparse_matrix() # = mass matrix of V1 + # multi-patch (broken) linear operators + H0 = derham_h.hodge_operator(space='V0', kind='linop', backend_language=backend_language) + H1 = derham_h.hodge_operator(space='V1', kind='linop', backend_language=backend_language) + dH0 = derham_h.hodge_operator(space='V0', kind='linop', dual=True, backend_language=backend_language) print('conforming projection operators...') # conforming Projections (should take into account the boundary conditions # of the continuous deRham sequence) - cP0_m = construct_h1_conforming_projection(V0h, hom_bc=True) - - def lift_u_bc(u_bc): - if u_bc is not None: - print('lifting the boundary condition in V0h... [warning: Not Tested Yet!]') - d_ubc_c = derham_h.get_dual_dofs(space='V0', f=u_bc, backend_language=backend_language, return_format='numpy_array') - ubc_c = dH0_m.dot(d_ubc_c) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', hom_bc = True) - ubc_c = ubc_c - cP0_m.dot(ubc_c) - else: - ubc_c = None - return ubc_c + print('building the discrete operators:') - # Conga (projection-based) stiffness matrices: - # div grad: - pre_DG_m = - bD0_m.transpose() @ H1_m @ bD0_m + I0 = IdentityOperator(V0h.coeff_space) + + # div grad + DG = - bD0.T @ H1 @ bD0 # jump penalization: - jump_penal_m = I0_m - cP0_m - JP0_m = jump_penal_m.transpose() @ H0_m @ jump_penal_m + JP0 = (I0 - cP0).T @ H0 @ (I0 - cP0) # useful for the boundary condition (if present) - pre_A_m = cP0_m.transpose() @ (eta * H0_m - mu * pre_DG_m) - A_m = pre_A_m @ cP0_m + gamma_h * JP0_m + pre_A = cP0.T @ (eta * H0 - mu * DG) + + A = pre_A @ cP0 + gamma_h * JP0 + + + f_scal, u_bc, u_ex = get_source_and_solution_h1(source_type=source_type, eta=eta, mu=mu, domain=domain, domain_name=domain_name,) - print('getting the source and ref solution...') - f_scal, u_bc, u_ex = get_source_and_solution_h1( - source_type=source_type, eta=eta, mu=mu, domain=domain, domain_name=domain_name, - ) + df = get_dual_dofs(Vh=V0h, f=f_scal, domain_h=domain_h, backend_language=backend_language) + f = dH0 @ df + df = cP0.T @ df - # compute approximate source f_h - b_c = derham_h.get_dual_dofs(space='V0', f=f_scal, backend_language=backend_language, return_format='numpy_array') - # source in primal sequence for plotting - f_c = dH0_m.dot(b_c) - b_c = cP0_m.transpose() @ b_c + def lift_u_bc(u_bc): + if u_bc is not None: + du_bc = get_dual_dofs(Vh=V0h, f=u_bc, domain_h = domain_h, backend_language=backend_language) + ubc = dH0.dot(du_bc) + ubc -= cP0.dot(ubc) - ubc_c = lift_u_bc(u_bc) + else: + ubc = None + + return ubc - if ubc_c is not None: + ubc = lift_u_bc(u_bc) + + if ubc is not None: # modified source for the homogeneous pbm print('modifying the source with lifted bc solution...') - b_c = b_c - pre_A_m.dot(ubc_c) + df -= pre_A @ ubc # direct solve with scipy spsolve - print('solving source problem with scipy.spsolve...') - uh_c = spsolve(A_m, b_c) + print('solving source problem with conjugate gradient...') + solver = inverse(A, solver='cg', tol=1e-8) + u = solver.solve(df) # project the homogeneous solution on the conforming problem space print('projecting the homogeneous solution on the conforming problem space...') - uh_c = cP0_m.dot(uh_c) + u = cP0.dot(u) - if ubc_c is not None: + if ubc is not None: # adding the lifted boundary condition print('adding the lifted boundary condition...') - uh_c += ubc_c + u += ubc - print('getting and plotting the FEM solution from numpy coefs array...') if u_ex: - u_ex_c = derham_h.get_dual_dofs(space='V0', f=u_ex, backend_language=backend_language, return_format='numpy_array') - u_ex_c = dH0_m.dot(u_ex_c) + u_ex = get_dual_dofs(Vh=V0h, f=u_ex, domain_h=domain_h, backend_language=backend_language) + u_ex = dH0.dot(u_ex) + if plot_dir is not None: + print('plotting the FEM solution...') + if not os.path.exists(plot_dir): os.makedirs(plot_dir) @@ -217,17 +184,14 @@ def lift_u_bc(u_bc): OM.add_spaces(V0h=V0h) OM.set_static() - stencil_coeffs = array_to_psydac(uh_c, V0h.coeff_space) - vh = FemField(V0h, coeffs=stencil_coeffs) - OM.export_fields(vh=vh) + uh = FemField(V0h, coeffs=u) + OM.export_fields(uh=uh) - stencil_coeffs = array_to_psydac(f_c, V0h.coeff_space) - fh = FemField(V0h, coeffs=stencil_coeffs) + fh = FemField(V0h, coeffs=f) OM.export_fields(fh=fh) if u_ex: - stencil_coeffs = array_to_psydac(u_ex_c, V0h.coeff_space) - uh_ex = FemField(V0h, coeffs=stencil_coeffs) + uh_ex = FemField(V0h, coeffs=u_ex) OM.export_fields(uh_ex=uh_ex) OM.export_space_info() @@ -243,7 +207,7 @@ def lift_u_bc(u_bc): grid=None, npts_per_cell=[6] * 2, snapshots='all', - fields='vh') + fields='uh') PM.export_to_vtk( plot_dir + "/f_h", @@ -263,9 +227,9 @@ def lift_u_bc(u_bc): PM.close() if u_ex: - err = uh_c - u_ex_c - rel_err = np.sqrt(np.dot(err, H0_m.dot(err)))/np.sqrt(np.dot(u_ex_c,H0_m.dot(u_ex_c))) - + err = u - u_ex + rel_err = np.sqrt(H0.dot_inner(err, err) / H0.dot_inner(u_ex, u_ex)) + return rel_err @@ -273,19 +237,21 @@ def lift_u_bc(u_bc): omega = np.sqrt(170) # source eta = -omega**2 + mu=0 + gamma_h = 10 source_type = 'manu_poisson_elliptic' domain_name = 'pretzel_f' - nc = 10 + nc = 4 deg = 2 run_dir = '{}_{}_nc={}_deg={}/'.format(domain_name, source_type, nc, deg) solve_h1_source_pbm( nc=nc, deg=deg, eta=eta, - mu=1, # 1, + mu=mu, # 1, domain_name=domain_name, source_type=source_type, backend_language='pyccel-gcc', diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py index db54565c7..418118469 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py @@ -2,41 +2,32 @@ Solve the eigenvalue problem for the curl-curl operator in 2D with a FEEC discretization """ import os -from mpi4py import MPI - import numpy as np -import matplotlib.pyplot as plt -from collections import OrderedDict + from sympde.topology import Derham -from psydac.feec.multipatch.api import discretize +from psydac.api.discretization import discretize from psydac.api.settings import PSYDAC_BACKENDS -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from psydac.feec.multipatch.operators import HodgeOperator from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -from psydac.feec.multipatch.utilities import time_count, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn -from psydac.feec.multipatch.utils_conga_2d import write_diags_to_file +from psydac.feec.multipatch.utilities import time_count -from sympde.topology import Square -from sympde.topology import IdentityMapping, PolarMapping from scipy.sparse.linalg import spilu, lgmres from scipy.sparse.linalg import LinearOperator, eigsh, minres -from scipy.sparse import csr_matrix from scipy.linalg import norm +from psydac.linalg.basic import IdentityOperator from psydac.linalg.utilities import array_to_psydac from psydac.fem.basic import FemField from psydac.feec.multipatch.multipatch_domain_utilities import build_cartesian_multipatch_domain -from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection from psydac.api.postprocessing import OutputManager, PostProcessManager def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), domain=([0, np.pi], [0, np.pi]), domain_name='refined_square', backend_language='pyccel-gcc', mu=1, nu=0, gamma_h=0, generalized_pbm=False, sigma=5, nb_eigs_solve=8, nb_eigs_plot=5, skip_eigs_threshold=1e-7, - plot_dir=None, m_load_dir=None,): + plot_dir=None): """ Solve the eigenvalue problem for the curl-curl operator in 2D with DG discretization @@ -70,8 +61,6 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma Threshold for the eigenvalues to skip plot_dir : str Directory for the plots - m_load_dir : str - Directory to save and load the matrices """ diags = {} @@ -110,10 +99,6 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma elif ncells.ndim == 2: ncells = {patch.name: [ncells[int(patch.name[2])][int(patch.name[4])], ncells[int(patch.name[2])][int(patch.name[4])]] for patch in domain.interior} - - mappings = OrderedDict([(P.logical_domain, P.mapping) - for P in domain.interior]) - mappings_list = list(mappings.values()) t_stamp = time_count(t_stamp) print(' .. discrete domain...') @@ -128,9 +113,7 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma print(' .. discrete derham sequence...') derham_h = discretize(derham, domain_h, degree=degree) - V0h = derham_h.V0 - V1h = derham_h.V1 - V2h = derham_h.V2 + V0h, V1h, V2h = derham_h.spaces print('dim(V0h) = {}'.format(V0h.nbasis)) print('dim(V1h) = {}'.format(V1h.nbasis)) print('dim(V2h) = {}'.format(V2h.nbasis)) @@ -142,64 +125,27 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma print('building the discrete operators:') print('commuting projection operators...') - I1 = IdLinearOperator(V1h) - I1_m = I1.to_sparse_matrix() + I1 = IdentityOperator(V1h.coeff_space) t_stamp = time_count(t_stamp) print('Hodge operators...') # multi-patch (broken) linear operators / matrices - H0 = HodgeOperator( - V0h, - domain_h, - backend_language=backend_language, - load_dir=m_load_dir, - load_space_index=0) - H1 = HodgeOperator( - V1h, - domain_h, - backend_language=backend_language, - load_dir=m_load_dir, - load_space_index=1) - H2 = HodgeOperator( - V2h, - domain_h, - backend_language=backend_language, - load_dir=m_load_dir, - load_space_index=2) - - H0_m = H0.to_sparse_matrix() # = mass matrix of V0 - dH0_m = H0.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V0 - H1_m = H1.to_sparse_matrix() # = mass matrix of V1 - dH1_m = H1.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V1 - H2_m = H2.to_sparse_matrix() # = mass matrix of V2 - dH2_m = H2.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V2 + H0, H1, H2 = derham_h.hodge_operators(kind='linop', backend_language=backend_language) + dH0, dH1, dH2 = derham_h.hodge_operators(kind='linop', dual=True, backend_language=backend_language) t_stamp = time_count(t_stamp) print('conforming projection operators...') # conforming Projections (should take into account the boundary conditions # of the continuous deRham sequence) - cP0_m = construct_h1_conforming_projection(V0h, hom_bc=True) - cP1_m = construct_hcurl_conforming_projection(V1h, hom_bc=True) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', hom_bc = True) - t_stamp = time_count(t_stamp) - print('broken differential operators...') - bD0, bD1 = derham_h.broken_derivatives_as_operators - bD0_m = bD0.to_sparse_matrix() - bD1_m = bD1.to_sparse_matrix() t_stamp = time_count(t_stamp) - print('converting some matrices to csr format...') - - H1_m = H1_m.tocsr() - dH1_m = dH1_m.tocsr() - H2_m = H2_m.tocsr() - bD1_m = bD1_m.tocsr() + print('broken differential operators...') + bD0, bD1 = derham_h.derivatives(kind='linop') - if not os.path.exists(plot_dir): - os.makedirs(plot_dir) print('computing the full operator matrix...') - A_m = np.zeros_like(H1_m) # Conga (projection-based) stiffness matrices if mu != 0: @@ -208,33 +154,29 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma print('mu = {}'.format(mu)) print('curl-curl stiffness matrix...') - pre_CC_m = bD1_m.transpose() @ H2_m @ bD1_m - CC_m = cP1_m.transpose() @ pre_CC_m @ cP1_m # Conga stiffness matrix - A_m += mu * CC_m + CC = cP1.T @ bD1.T @ H2 @ bD1 @ cP1 # Conga stiffness matrix + A = mu * CC if nu != 0: - pre_GD_m = - H1_m @ bD0_m @ cP0_m @ dH0_m @ cP0_m.transpose() @ bD0_m.transpose() @ H1_m - GD_m = cP1_m.transpose() @ pre_GD_m @ cP1_m # Conga stiffness matrix - A_m -= nu * GD_m + GD = - cP1.T @ H1 @ bD0 @ cP0 @ dH0 @ cP0.T @ bD0.T @ H1 @ cP1 + A -= nu * GD # jump stabilization in V1h: if gamma_h != 0 or generalized_pbm: t_stamp = time_count(t_stamp) print('jump stabilization matrix...') - jump_stab_m = I1_m - cP1_m - JS_m = jump_stab_m.transpose() @ H1_m @ jump_stab_m - A_m += gamma_h * JS_m + JS = (I1 - cP1).T @ H1 @ (I1 - cP1) + A += gamma_h * JS if generalized_pbm: print('adding jump stabilization to RHS of generalized eigenproblem...') - B_m = cP1_m.transpose() @ H1_m @ cP1_m + JS_m + B = cP1.T @ H1 @ cP1 + JS else: - B_m = H1_m + B = H1 t_stamp = time_count(t_stamp) print('solving matrix eigenproblem...') - all_eigenvalues, all_eigenvectors_transp = get_eigenvalues( - nb_eigs_solve, sigma, A_m, B_m) + all_eigenvalues, all_eigenvectors_transp = get_eigenvalues(nb_eigs_solve, sigma, A.tosparse(), B.tosparse()) # Eigenvalue processing t_stamp = time_count(t_stamp) print('sorting out eigenvalues...') @@ -262,37 +204,45 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma t_stamp = time_count(t_stamp) print('plotting the eigenmodes...') - OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') - OM.add_spaces(V1h=V1h) - OM.export_space_info() - - nb_eigs = len(eigenvalues) - for i in range(min(nb_eigs_plot, nb_eigs)): - - print('looking at emode i = {}... '.format(i)) - lambda_i = eigenvalues[i] - emode_i = np.real(eigenvectors[i]) - norm_emode_i = np.dot(emode_i, H1_m.dot(emode_i)) - eh_c = emode_i / norm_emode_i - - stencil_coeffs = array_to_psydac(cP1_m @ eh_c, V1h.coeff_space) - vh = FemField(V1h, coeffs=stencil_coeffs) - OM.add_snapshot(i, i) - OM.export_fields(vh=vh) - - OM.close() - - PM = PostProcessManager( - domain=domain, - space_file=plot_dir + '/spaces.yml', - fields_file=plot_dir + '/fields.h5') - PM.export_to_vtk( - plot_dir + "/eigenvalues", - grid=None, - npts_per_cell=[6] * 2, - snapshots='all', - fields='vh') - PM.close() + if plot_dir: + + if not os.path.exists(plot_dir): + os.makedirs(plot_dir) + + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') + OM.add_spaces(V1h=V1h) + OM.export_space_info() + + nb_eigs = len(eigenvalues) + H1_m = H1.tosparse() + cP1_m = cP1.tosparse() + + for i in range(min(nb_eigs_plot, nb_eigs)): + + print('looking at emode i = {}... '.format(i)) + lambda_i = eigenvalues[i] + emode_i = np.real(eigenvectors[i]) + norm_emode_i = np.dot(emode_i, H1_m.dot(emode_i)) + eh_c = emode_i / norm_emode_i + + stencil_coeffs = array_to_psydac(cP1_m @ eh_c, V1h.coeff_space) + vh = FemField(V1h, coeffs=stencil_coeffs) + OM.add_snapshot(i, i) + OM.export_fields(vh=vh) + + OM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + '/spaces.yml', + fields_file=plot_dir + '/fields.h5') + PM.export_to_vtk( + plot_dir + "/eigenvalues", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='vh') + PM.close() t_stamp = time_count(t_stamp) diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py index 58c6a2bb6..0235dd72d 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py @@ -3,14 +3,13 @@ A. Buffa and I. Perugia, “Discontinuous Galerkin Approximation of the Maxwell Eigenproblem” SIAM Journal on Numerical Analysis 44 (2006) """ - import os from mpi4py import MPI from collections import OrderedDict import numpy as np import matplotlib.pyplot -from scipy.sparse.linalg import spsolve, inv + from scipy.sparse.linalg import LinearOperator, eigsh, minres from sympde.calculus import grad, dot, curl, cross @@ -26,14 +25,12 @@ from sympde.expr.equation import find, EssentialBC from psydac.linalg.utilities import array_to_psydac -from psydac.api.tests.build_domain import build_pretzel from psydac.fem.basic import FemField -from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.feec.pull_push import pull_2d_hcurl from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -from psydac.feec.multipatch.utilities import time_count, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn -from psydac.feec.multipatch.api import discretize +from psydac.feec.multipatch.utilities import time_count +from psydac.api.discretization import discretize from psydac.feec.multipatch.multipatch_domain_utilities import build_cartesian_multipatch_domain from psydac.api.postprocessing import OutputManager, PostProcessManager @@ -194,40 +191,41 @@ def avr(w): return 0.5 * plus(w) + 0.5 * minus(w) t_stamp = time_count(t_stamp) print('plotting the eigenmodes...') - if not os.path.exists(plot_dir): - os.makedirs(plot_dir) - - OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') - OM.add_spaces(Vh=Vh) - OM.export_space_info() - - nb_eigs = len(eigenvalues) - for i in range(min(nb_eigs_plot, nb_eigs)): - - print('looking at emode i = {}... '.format(i)) - lambda_i = eigenvalues[i] - emode_i = np.real(eigenvectors[i]) - norm_emode_i = np.dot(emode_i, Bh_m.dot(emode_i)) - eh_c = emode_i / norm_emode_i - - stencil_coeffs = array_to_psydac(eh_c, Vh.coeff_space) - vh = FemField(Vh, coeffs=stencil_coeffs) - OM.add_snapshot(i, i) - OM.export_fields(vh=vh) - - OM.close() - - PM = PostProcessManager( - domain=domain, - space_file=plot_dir + '/spaces.yml', - fields_file=plot_dir + '/fields.h5') - PM.export_to_vtk( - plot_dir + "/eigenvalues", - grid=None, - npts_per_cell=[6] * 2, - snapshots='all', - fields='vh') - PM.close() + if plot_dir: + if not os.path.exists(plot_dir): + os.makedirs(plot_dir) + + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') + OM.add_spaces(Vh=Vh) + OM.export_space_info() + + nb_eigs = len(eigenvalues) + for i in range(min(nb_eigs_plot, nb_eigs)): + + print('looking at emode i = {}... '.format(i)) + lambda_i = eigenvalues[i] + emode_i = np.real(eigenvectors[i]) + norm_emode_i = np.dot(emode_i, Bh_m.dot(emode_i)) + eh_c = emode_i / norm_emode_i + + stencil_coeffs = array_to_psydac(eh_c, Vh.coeff_space) + vh = FemField(Vh, coeffs=stencil_coeffs) + OM.add_snapshot(i, i) + OM.export_fields(vh=vh) + + OM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + '/spaces.yml', + fields_file=plot_dir + '/fields.h5') + PM.export_to_vtk( + plot_dir + "/eigenvalues", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='vh') + PM.close() t_stamp = time_count(t_stamp) diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py b/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py index 4f311a7eb..5e887beda 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py @@ -220,9 +220,6 @@ diag_filename = plot_dir + '/' + diag_fn() common_diag_filename = './' + case_dir + '_diags.txt' -# to save and load matrices -# m_load_dir = get_mat_dir(domain_name, nc, deg) -m_load_dir = None print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') print(' Calling hcurl_solve_eigen_pbm() with params = {}'.format(params)) @@ -254,7 +251,6 @@ domain_name=domain_name, domain=domain, backend_language=backend_language, plot_dir=plot_dir, - m_load_dir=m_load_dir, ) elif method == 'dg': diff --git a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py index b7442e054..e2252a0e2 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py @@ -14,42 +14,29 @@ """ import os -from mpi4py import MPI import numpy as np -from collections import OrderedDict -from sympy import lambdify, Matrix - -from scipy.sparse.linalg import spsolve - -from sympde.calculus import dot -from sympde.topology import element_of -from sympde.expr.expr import LinearForm -from sympde.expr.expr import integral, Norm from sympde.topology import Derham -from psydac.api.settings import PSYDAC_BACKENDS -from psydac.feec.pull_push import pull_2d_hcurl -from psydac.feec.multipatch.api import discretize -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from psydac.feec.multipatch.operators import HodgeOperator +from psydac.api.discretization import discretize from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_hcurl -from psydac.feec.multipatch.utils_conga_2d import DiagGrid, P0_phys, P1_phys, P2_phys, get_Vh_diags_for +from psydac.feec.multipatch.utils_conga_2d import P1_phys from psydac.feec.multipatch.utilities import time_count -from psydac.linalg.utilities import array_to_psydac +# from psydac.linalg.utilities import array_to_psydac from psydac.fem.basic import FemField -from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection from psydac.api.postprocessing import OutputManager, PostProcessManager +from psydac.linalg.basic import IdentityOperator +from psydac.fem.projectors import get_dual_dofs +from psydac.linalg.solvers import inverse + def solve_hcurl_source_pbm( - nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_proj='P_geom', source_type='manu_J', + nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_proj='tilde_Pi', source_type='manu_J', eta=-10., mu=1., nu=1., gamma_h=10., - project_sol=False, plot_dir=None, - m_load_dir=None, -): + project_sol=True, plot_dir=None): """ solver for the problem: find u in H(curl), such that @@ -82,15 +69,11 @@ def solve_hcurl_source_pbm( :param source_proj: approximation operator (in V1h) for the source, possible values are - 'tilde_Pi': dual commuting projection, an L2 projection filtered by the adjoint conforming projection) :param source_type: must be implemented in get_source_and_solution() - :param m_load_dir: directory for matrix storage """ diags = {} degree = [deg, deg] - if m_load_dir is not None: - if not os.path.exists(m_load_dir): - os.makedirs(m_load_dir) print('---------------------------------------------------------------------------------------------------------') print('Starting solve_hcurl_source_pbm function with: ') @@ -107,9 +90,9 @@ def solve_hcurl_source_pbm( t_stamp = time_count() print(' .. multi-patch domain...') domain = build_multipatch_domain(domain_name=domain_name) - mappings = OrderedDict([(P.logical_domain, P.mapping) - for P in domain.interior]) - mappings_list = list(mappings.values()) + # mappings = OrderedDict([(P.logical_domain, P.mapping) + # for P in domain.interior]) + # mappings_list = list(mappings.values()) if isinstance(nc, int): ncells = [nc, nc] @@ -117,8 +100,6 @@ def solve_hcurl_source_pbm( ncells = {patch.name: [nc[i], nc[i]] for (i, patch) in enumerate(domain.interior)} - # for diagnosttics - diag_grid = DiagGrid(mappings=mappings, N_diag=100) t_stamp = time_count(t_stamp) print(' .. derham sequence...') @@ -134,14 +115,14 @@ def solve_hcurl_source_pbm( t_stamp = time_count(t_stamp) print(' .. commuting projection operators...') - nquads = [4 * (d + 1) for d in degree] + nquads = [10 * (d + 1) for d in degree] P0, P1, P2 = derham_h.projectors(nquads=nquads) t_stamp = time_count(t_stamp) print(' .. multi-patch spaces...') - V0h = derham_h.V0 - V1h = derham_h.V1 - V2h = derham_h.V2 + V0h, V1h, V2h = derham_h.spaces + mappings = derham_h.callable_mapping + print('dim(V0h) = {}'.format(V0h.nbasis)) print('dim(V1h) = {}'.format(V1h.nbasis)) print('dim(V2h) = {}'.format(V2h.nbasis)) @@ -151,101 +132,44 @@ def solve_hcurl_source_pbm( t_stamp = time_count(t_stamp) print(' .. Id operator and matrix...') - I1 = IdLinearOperator(V1h) - I1_m = I1.to_sparse_matrix() + I1 = IdentityOperator(V1h.coeff_space) t_stamp = time_count(t_stamp) print(' .. Hodge operators...') # multi-patch (broken) linear operators / matrices # other option: define as Hodge Operators: - H0 = HodgeOperator( - V0h, - domain_h, - backend_language=backend_language, - load_dir=m_load_dir, - load_space_index=0) - H1 = HodgeOperator( - V1h, - domain_h, - backend_language=backend_language, - load_dir=m_load_dir, - load_space_index=1) - H2 = HodgeOperator( - V2h, - domain_h, - backend_language=backend_language, - load_dir=m_load_dir, - load_space_index=2) - - t_stamp = time_count(t_stamp) - print(' .. Hodge matrix H0_m = M0_m ...') - H0_m = H0.to_sparse_matrix() - t_stamp = time_count(t_stamp) - print(' .. dual Hodge matrix dH0_m = inv_M0_m ...') - dH0_m = H0.get_dual_Hodge_sparse_matrix() - - t_stamp = time_count(t_stamp) - print(' .. Hodge matrix H1_m = M1_m ...') - H1_m = H1.to_sparse_matrix() - t_stamp = time_count(t_stamp) - print(' .. dual Hodge matrix dH1_m = inv_M1_m ...') - dH1_m = H1.get_dual_Hodge_sparse_matrix() + H0, H1, H2 = derham_h.hodge_operators(kind='linop', backend_language=backend_language) + dH0, dH1, dH2 = derham_h.hodge_operators(kind='linop', dual=True, backend_language=backend_language) - t_stamp = time_count(t_stamp) - print(' .. Hodge matrix H2_m = M2_m ...') - H2_m = H2.to_sparse_matrix() - dH2_m = H2.get_dual_Hodge_sparse_matrix() t_stamp = time_count(t_stamp) print(' .. conforming Projection operators...') # conforming Projections (should take into account the boundary conditions # of the continuous deRham sequence) - cP0_m = construct_h1_conforming_projection(V0h, hom_bc=True) - cP1_m = construct_hcurl_conforming_projection(V1h, hom_bc=True) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', hom_bc = True) + t_stamp = time_count(t_stamp) print(' .. broken differential operators...') # broken (patch-wise) differential operators - bD0, bD1 = derham_h.broken_derivatives_as_operators - bD0_m = bD0.to_sparse_matrix() - bD1_m = bD1.to_sparse_matrix() - - if plot_dir is not None and not os.path.exists(plot_dir): - os.makedirs(plot_dir) - - def lift_u_bc(u_bc): - if u_bc is not None: - print('lifting the boundary condition in V1h...') - # note: for simplicity we apply the full P1 on u_bc, but we only - # need to set the boundary dofs - uh_bc = P1_phys(u_bc, P1, domain, mappings_list) - ubc_c = uh_bc.coeffs.toarray() - # removing internal dofs (otherwise ubc_c may already be a very - # good approximation of uh_c ...) - ubc_c = ubc_c - cP1_m.dot(ubc_c) - else: - ubc_c = None - return ubc_c + bD0, bD1 = derham_h.derivatives(kind='linop') # Conga (projection-based) stiffness matrices # curl curl: t_stamp = time_count(t_stamp) print(' .. curl-curl stiffness matrix...') - print(bD1_m.shape, H2_m.shape) - pre_CC_m = bD1_m.transpose() @ H2_m @ bD1_m - # CC_m = cP1_m.transpose() @ pre_CC_m @ cP1_m # Conga stiffness matrix + pre_CC = bD1.T @ H2 @ bD1 # grad div: t_stamp = time_count(t_stamp) print(' .. grad-div stiffness matrix...') - pre_GD_m = - H1_m @ bD0_m @ cP0_m @ dH0_m @ cP0_m.transpose() @ bD0_m.transpose() @ H1_m - # GD_m = cP1_m.transpose() @ pre_GD_m @ cP1_m # Conga stiffness matrix + pre_GD = - H1 @ bD0 @ cP0 @ dH0 @ cP0.T @ bD0.T @ H1 # jump stabilization: t_stamp = time_count(t_stamp) print(' .. jump stabilization matrix...') - jump_penal_m = I1_m - cP1_m - JP_m = jump_penal_m.transpose() @ H1_m @ jump_penal_m + JS = (I1 - cP1).T @ H1 @ (I1 - cP1) + t_stamp = time_count(t_stamp) print(' .. full operator matrix...') @@ -254,69 +178,81 @@ def lift_u_bc(u_bc): print('nu = {}'.format(nu)) print('STABILIZATION: gamma_h = {}'.format(gamma_h)) # useful for the boundary condition (if present) - pre_A_m = cP1_m.transpose() @ (eta * H1_m + mu * pre_CC_m - nu * pre_GD_m) - A_m = pre_A_m @ cP1_m + gamma_h * JP_m + pre_A = eta * cP1.T @ H1 + if mu != 0: + pre_A += mu * cP1.T @ pre_CC + if nu != 0: + pre_A -= nu * cP1.T @ pre_GD + + A = pre_A @ cP1 + gamma_h * JS t_stamp = time_count(t_stamp) print() print(' -- getting source --') - f_vect, u_bc, u_ex, curl_u_ex, div_u_ex = get_source_and_solution_hcurl( - source_type=source_type, eta=eta, mu=mu, domain=domain, domain_name=domain_name,) + f_vect, u_bc, u_ex, curl_u_ex, div_u_ex = get_source_and_solution_hcurl(source_type=source_type, eta=eta, mu=mu, domain=domain, domain_name=domain_name,) # compute approximate source f_h t_stamp = time_count(t_stamp) # f_h = L2 projection of f_vect, with filtering if tilde_Pi - print(' .. projecting the source with ' + - source_proj +' projection...') - - tilde_f_c = derham_h.get_dual_dofs( - space='V1', - f=f_vect, - backend_language=backend_language, - return_format='numpy_array') + print(' .. projecting the source with ' + source_proj +' projection...') + + tilde_f = get_dual_dofs(Vh=V1h, f=f_vect, domain_h=domain_h, backend_language=backend_language) + if source_proj == 'tilde_Pi': - print(' .. filtering the discrete source with P0.T ...') - tilde_f_c = cP1_m.transpose() @ tilde_f_c + print(' .. filtering the discrete source with P1.T ...') + tilde_f = cP1.T @ tilde_f + + def lift_u_bc(u_bc): + if u_bc is not None: + ubc = P1_phys(u_bc, P1, domain).coeffs + ubc -= cP1.dot(ubc) + + else: + ubc = None + + return ubc - ubc_c = lift_u_bc(u_bc) - if ubc_c is not None: + ubc = lift_u_bc(u_bc) + + if ubc is not None: # modified source for the homogeneous pbm t_stamp = time_count(t_stamp) print(' .. modifying the source with lifted bc solution...') - tilde_f_c = tilde_f_c - pre_A_m.dot(ubc_c) + tilde_f -= pre_A.dot(ubc) # direct solve with scipy spsolve t_stamp = time_count(t_stamp) - print() - print(' -- solving source problem with scipy.spsolve...') - uh_c = spsolve(A_m, tilde_f_c) + print('solving source problem with conjugate gradient...') + solver = inverse(A, solver='cg', tol=1e-8) + u = solver.solve(tilde_f) # project the homogeneous solution on the conforming problem space + t_stamp = time_count(t_stamp) if project_sol: - t_stamp = time_count(t_stamp) print(' .. projecting the homogeneous solution on the conforming problem space...') - uh_c = cP1_m.dot(uh_c) - else: - print(' .. NOT projecting the homogeneous solution on the conforming problem space') + u = cP1.dot(u) - if ubc_c is not None: + if ubc is not None: # adding the lifted boundary condition t_stamp = time_count(t_stamp) print(' .. adding the lifted boundary condition...') - uh_c += ubc_c + u += ubc - uh = FemField(V1h, coeffs=array_to_psydac(uh_c, V1h.coeff_space)) + uh = FemField(V1h, coeffs=u) #need cp1 here? - f_c = dH1_m.dot(tilde_f_c) - jh = FemField(V1h, coeffs=array_to_psydac(f_c, V1h.coeff_space)) + f = dH1.dot(tilde_f) + jh = FemField(V1h, coeffs=f) t_stamp = time_count(t_stamp) print(' -- plots and diagnostics --') if plot_dir: + if not os.path.exists(plot_dir): + os.makedirs(plot_dir) + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') OM.add_spaces(V1h=V1h) OM.set_static() @@ -349,11 +285,12 @@ def lift_u_bc(u_bc): time_count(t_stamp) if u_ex: - u_ex_c = P1_phys(u_ex, P1, domain, mappings_list).coeffs.toarray() - err = u_ex_c - uh_c - l2_error = np.sqrt(np.dot(err, H1_m.dot(err)))/np.sqrt(np.dot(u_ex_c,H1_m.dot(u_ex_c))) + u_ex_p = P1_phys(u_ex, P1, domain).coeffs + + err = u_ex_p - u + print(err.inner(H1.dot(err))) + l2_error = np.sqrt( H1.dot_inner(err, err) / H1.dot_inner(u_ex_p, u_ex_p)) print(l2_error) - #return l2_error diags['err'] = l2_error return diags diff --git a/psydac/feec/multipatch/examples/hcurl_source_testcase.py b/psydac/feec/multipatch/examples/hcurl_source_testcase.py index 35aa79dd6..720fb75b5 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_testcase.py +++ b/psydac/feec/multipatch/examples/hcurl_source_testcase.py @@ -94,9 +94,6 @@ diag_filename = plot_dir + '/' + \ diag_fn(source_type=source_type, source_proj=source_proj) - # to save and load matrices - m_load_dir = get_mat_dir(domain_name, nc, deg) - # to save the FEM sol print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') @@ -124,7 +121,6 @@ project_sol=project_sol, gamma_h=gamma_h, plot_dir=plot_dir, - m_load_dir=m_load_dir, ) # diff --git a/psydac/feec/multipatch/examples/ppc_test_cases.py b/psydac/feec/multipatch/examples/ppc_test_cases.py index 94b772f4d..f0f7d0f8c 100644 --- a/psydac/feec/multipatch/examples/ppc_test_cases.py +++ b/psydac/feec/multipatch/examples/ppc_test_cases.py @@ -1,24 +1,9 @@ # coding: utf-8 - -from sympy.functions.special.error_functions import erf -from mpi4py import MPI - import os import numpy as np from sympy import pi, cos, sin, Tuple, exp, atan, atan2 - -from sympde.topology import Derham - -from psydac.fem.basic import FemField -from psydac.feec.multipatch.api import discretize -from psydac.feec.multipatch.operators import HodgeOperator -from psydac.fem.plotting_utilities import get_plotting_grid, my_small_plot, my_small_streamplot -from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain - -comm = MPI.COMM_WORLD - - +from sympy.functions.special.error_functions import erf # todo [MCP, 12/02/2022]: add an 'equation' argument to be able to return # 'exact solution' diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell.py b/psydac/feec/multipatch/examples/timedomain_maxwell.py index 9e772f3e5..f382f0111 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell.py @@ -30,25 +30,22 @@ from sympde.expr.expr import LinearForm from sympde.expr.expr import integral, Norm from sympde.topology import Derham +from psydac.linalg.basic import IdentityOperator from psydac.api.settings import PSYDAC_BACKENDS -from psydac.feec.pull_push import pull_2d_hcurl -from psydac.feec.multipatch.api import discretize -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from psydac.feec.multipatch.operators import HodgeOperator, get_K0_and_K0_inv, get_K1_and_K1_inv -# , write_field_to_diag_grid, +from psydac.api.discretization import discretize + from psydac.fem.plotting_utilities import plot_field_2d as plot_field from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -# , get_praxial_Gaussian_beam_E, get_easy_Gaussian_beam_E, get_easy_Gaussian_beam_B,get_easy_Gaussian_beam_E_2, get_easy_Gaussian_beam_B_2 + from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_hcurl, get_div_free_pulse, get_curl_free_pulse, get_Delta_phi_pulse, get_Gaussian_beam from psydac.feec.multipatch.utils_conga_2d import DiagGrid, P0_phys, P1_phys, P2_phys, get_Vh_diags_for -from psydac.feec.multipatch.utilities import time_count # , export_sol, import_sol -from psydac.linalg.utilities import array_to_psydac +from psydac.feec.multipatch.utilities import time_count from psydac.fem.basic import FemField -from psydac.feec.multipatch.non_matching_operators import construct_hcurl_conforming_projection, construct_h1_conforming_projection from psydac.feec.multipatch.multipatch_domain_utilities import build_cartesian_multipatch_domain from psydac.api.postprocessing import OutputManager, PostProcessManager +from psydac.fem.projectors import get_dual_dofs def solve_td_maxwell_pbm(*, @@ -61,26 +58,13 @@ def solve_td_maxwell_pbm(*, backend='pyccel-gcc', source_type='zero', source_omega=None, - source_proj='P_geom', - conf_proj='BSP', - gamma_h=10., + source_proj='P_L2', project_sol=False, filter_source=True, - quad_param=1, - E0_type='zero', + E0_type='pulse_2', E0_proj='P_L2', - hide_plots=True, plot_dir=None, plot_time_ranges=None, - plot_source=False, - plot_divE=False, - diag_dt=None, - # diag_dtau = None, - cb_min_sol=None, - cb_max_sol=None, - m_load_dir=None, - th_sol_filename="", - source_is_harmonic=False, domain_lims=None ): """ @@ -145,16 +129,6 @@ def solve_td_maxwell_pbm(*, dual degrees of freedom. Change of basis from primal to dual (and vice versa) is obtained through multiplication with the proper Hodge matrix. - conf_proj : str {'BSP' | 'GSP'} - Kind of conforming projection operator. Choose 'BSP' for an operator - based on the spline coefficients, which has maximum data locality. - Choose 'GSP' for an operator based on the geometric degrees of freedom, - which requires a change of basis (from B-spline to geometric, and then - vice versa) on the patch interfaces. - - gamma_h : float - Jump penalization parameter. - project_sol : bool Whether the solution fields should be projected onto the corresponding conforming spaces before plotting them. @@ -163,23 +137,14 @@ def solve_td_maxwell_pbm(*, If True, the current source will be filtered with the conforming projector operator (or its dual, depending on which basis is used). - quad_param : int - Multiplicative factor for the number of quadrature points; set - `quad_param` > 1 if you suspect that the quadrature is not accurate. - - E0_type : str {'zero', 'th_sol', 'pulse'} - Initial conditions for the electric field. Choose 'zero' for E0=0, - 'th_sol' for a field obtained from the time-harmonic Maxwell solver - (must provide a time-harmonic current source and set `source_omega`), + E0_type : str {'zero', 'pulse'} + Initial conditions for the electric field. Choose 'zero' for E0=0 and 'pulse' for a non-zero field localized in a small region. E0_proj : str {'P_geom' | 'P_L2'} Name of the approximation operator for the initial electric field E0 (see source_proj for details). Only relevant if E0 is not zero. - hide_plots : bool - If True, no windows are opened to show the figures interactively. - plot_dir : str Path to the directory where the figures will be saved. @@ -187,33 +152,12 @@ def solve_td_maxwell_pbm(*, List of lists, of the form `[[start, end], dtp]`, where `[start, end]` is a time interval and `dtp` is the time between two successive plots. - plot_source : bool - If True, plot the discrete field that approximates the current source. - - plot_divE : bool - If True, compute and plot the (weak) divergence of the electric field. - - diag_dt : float - Time elapsed between two successive calculations of scalar diagnostic - quantities. - - cb_min_sol : float - Minimum value to be used in colorbars when visualizing the solution. - - cb_max_sol : float - Maximum value to be used in colorbars when visualizing the solution. - - m_load_dir : str - Path to directory for matrix storage. - - th_sol_filename : str - Path to file with time-harmonic solution (to be used in conjuction with - `source_is_harmonic = True` and `E0_type = 'th_sol'`). + domain_lims : list + If the domain_name is 'refined_square' or 'square_L_shape', this + parameter must be set to the list of the two intervals defining the + rectangular domain, i.e. `[[x_min, x_max], [y_min, y_max]]`. """ - diags = {} - - # ncells = [nc, nc] degree = [deg, deg] if source_omega is not None: @@ -225,18 +169,6 @@ def solve_td_maxwell_pbm(*, [[0, final_time], final_time] ] - if diag_dt is None: - diag_dt = 0.1 - - # if backend is None: - # if domain_name in ['pretzel', 'pretzel_f'] and nc > 8: - # backend = 'numba' - # else: - # backend = 'python' - # print('[note: using '+backend_language+ ' backends in discretize functions]') - if m_load_dir is not None: - if not os.path.exists(m_load_dir): - os.makedirs(m_load_dir) print('---------------------------------------------------------------------------------------------------------') print('Starting solve_td_maxwell_pbm function with: ') @@ -248,10 +180,8 @@ def solve_td_maxwell_pbm(*, print(' source_type = {}'.format(source_type)) print(' source_proj = {}'.format(source_proj)) print(' backend = {}'.format(backend)) - # TODO: print other parameters print('---------------------------------------------------------------------------------------------------------') - debug = False print() print(' -- building discrete spaces and operators --') @@ -267,10 +197,10 @@ def solve_td_maxwell_pbm(*, if isinstance(nc, int): ncells = [nc, nc] - elif ncells.ndim == 1: + elif nc.ndim == 1: ncells = {patch.name: [nc[i], nc[i]] for (i, patch) in enumerate(domain.interior)} - elif ncells.ndim == 2: + elif nc.ndim == 2: ncells = {patch.name: [nc[int(patch.name[2])][int(patch.name[4])], nc[int(patch.name[2])][int(patch.name[4])]] for patch in domain.interior} @@ -278,8 +208,6 @@ def solve_td_maxwell_pbm(*, for P in domain.interior]) mappings_list = list(mappings.values()) - # for diagnosttics - diag_grid = DiagGrid(mappings=mappings, N_diag=100) t_stamp = time_count(t_stamp) print(' .. derham sequence...') @@ -301,101 +229,37 @@ def solve_td_maxwell_pbm(*, t_stamp = time_count(t_stamp) print(' .. multi-patch spaces...') - V0h = derham_h.V0 - V1h = derham_h.V1 - V2h = derham_h.V2 - print('dim(V0h) = {}'.format(V0h.nbasis)) - print('dim(V1h) = {}'.format(V1h.nbasis)) - print('dim(V2h) = {}'.format(V2h.nbasis)) - diags['ndofs_V0'] = V0h.nbasis - diags['ndofs_V1'] = V1h.nbasis - diags['ndofs_V2'] = V2h.nbasis + V0h, V1h, V2h = derham_h.spaces t_stamp = time_count(t_stamp) print(' .. Id operator and matrix...') - I1 = IdLinearOperator(V1h) - I1_m = I1.to_sparse_matrix() + I1 = IdentityOperator(V1h.coeff_space) t_stamp = time_count(t_stamp) print(' .. Hodge operators...') - # multi-patch (broken) linear operators / matrices - # other option: define as Hodge Operators: - H0 = HodgeOperator( - V0h, - domain_h, - backend_language=backend, - load_dir=m_load_dir, - load_space_index=0) - H1 = HodgeOperator( - V1h, - domain_h, - backend_language=backend, - load_dir=m_load_dir, - load_space_index=1) - H2 = HodgeOperator( - V2h, - domain_h, - backend_language=backend, - load_dir=m_load_dir, - load_space_index=2) - - t_stamp = time_count(t_stamp) - print(' .. Hodge matrix H0_m = M0_m ...') - H0_m = H0.to_sparse_matrix() - t_stamp = time_count(t_stamp) - print(' .. dual Hodge matrix dH0_m = inv_M0_m ...') - dH0_m = H0.get_dual_Hodge_sparse_matrix() + H0, H1, H2 = derham_h.hodge_operators(kind='linop') + dH0, dH1, dH2 = derham_h.hodge_operators(kind='linop', dual=True) - t_stamp = time_count(t_stamp) - print(' .. Hodge matrix H1_m = M1_m ...') - H1_m = H1.to_sparse_matrix() - t_stamp = time_count(t_stamp) - print(' .. dual Hodge matrix dH1_m = inv_M1_m ...') - dH1_m = H1.get_dual_Hodge_sparse_matrix() - - t_stamp = time_count(t_stamp) - print(' .. Hodge matrix dH2_m = M2_m ...') - H2_m = H2.to_sparse_matrix() - print(' .. dual Hodge matrix dH2_m = inv_M2_m ...') - dH2_m = H2.get_dual_Hodge_sparse_matrix() t_stamp = time_count(t_stamp) print(' .. conforming Projection operators...') - cP0_m = construct_h1_conforming_projection(V0h, hom_bc=False) - cP1_m = construct_hcurl_conforming_projection(V1h, hom_bc=False) - - if conf_proj == 'GSP': - print(' [* GSP-conga: using Geometric Spline conf Projections ]') - K0, K0_inv = get_K0_and_K0_inv(V0h, uniform_patches=False) - cP0_m = K0_inv @ cP0_m @ K0 - K1, K1_inv = get_K1_and_K1_inv(V1h, uniform_patches=False) - cP1_m = K1_inv @ cP1_m @ K1 - elif conf_proj == 'BSP': - print(' [* BSP-conga: using B-Spline conf Projections ]') - else: - raise ValueError(conf_proj) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', p_moments = degree[0]+2, hom_bc = False) t_stamp = time_count(t_stamp) print(' .. broken differential operators...') - # broken (patch-wise) differential operators - bD0, bD1 = derham_h.broken_derivatives_as_operators - bD0_m = bD0.to_sparse_matrix() - bD1_m = bD1.to_sparse_matrix() + bD0, bD1 = derham_h.derivatives(kind='linop') + if plot_dir is not None and not os.path.exists(plot_dir): os.makedirs(plot_dir) - # Conga (projection-based) matrices - t_stamp = time_count(t_stamp) - dH1_m = dH1_m.tocsr() - H2_m = H2_m.tocsr() - cP1_m = cP1_m.tocsr() - bD1_m = bD1_m.tocsr() - print(' .. matrix of the primal curl (in primal bases)...') - C_m = bD1_m @ cP1_m + C = bD1 @ cP1 print(' .. matrix of the dual curl (also in primal bases)...') + dC = dH1 @ C.T @ H2 + + ### Silvermueller ABC from sympde.calculus import grad, dot, curl, cross from sympde.topology import NormalVector from sympde.expr.expr import BilinearForm @@ -408,37 +272,38 @@ def solve_td_maxwell_pbm(*, a = BilinearForm((u, v), integral(boundary, expr_b)) ah = discretize(a, domain_h, [V1h, V1h], backend=PSYDAC_BACKENDS[backend],) - A_eps = ah.assemble().tosparse() + A_eps = ah.assemble() + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - dC_m = dH1_m @ C_m.transpose() @ H2_m # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Compute stable time step size based on max CFL and max dt - dt = compute_stable_dt(C_m=C_m, dC_m=dC_m, cfl_max=cfl_max, dt_max=dt_max) + dt = compute_stable_dt(C=C, dC=dC, cfl_max=cfl_max, dt_max=dt_max) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Absorbing dC_m - CH2 = C_m.transpose() @ H2_m - H1A = H1_m + dt * A_eps - - H1A_csc = H1A.tocsc() - dC_m = sp.sparse.linalg.spsolve(H1A_csc, CH2.tocsc()) - dCH1_m = sp.sparse.linalg.spsolve(H1A_csc, H1_m.tocsc()) + # Absorbing dC + CH2 = C.T @ H2 + H1A = H1 + dt * A_eps + + # alternative inverse + # from psydac.linalg.solvers import inverse + # H1A_inv = inverse(H1A, solver='cg', tol=1e-8) + ### + M = H1A + from scipy.linalg import inv + from scipy.sparse import csr_matrix + from psydac.linalg.sparse import SparseMatrixLinearOperator + M_inv = inv(M.toarray()) + M_inv = csr_matrix(M_inv) + H1A_inv = SparseMatrixLinearOperator(M.codomain, M.domain, M_inv) + #### + + dC = H1A_inv @ CH2 + dCH1 = H1A_inv @ H1 print(' .. matrix of the dual div (still in primal bases)...') - div_m = dH0_m @ cP0_m.transpose() @ bD0_m.transpose() @ H1_m - - # jump stabilization (may not be needed) - t_stamp = time_count(t_stamp) - print(' .. jump stabilization matrix...') - jump_penal_m = I1_m - cP1_m - JP_m = jump_penal_m.transpose() * H1_m * jump_penal_m + D = dH0 @ cP0.T @ bD0.T @ H1 - # t_stamp = time_count(t_stamp) - # print(' .. full operator matrix...') - # print('STABILIZATION: gamma_h = {}'.format(gamma_h)) - # pre_A_m = cP1_m.transpose() @ ( eta * H1_m + mu * pre_CC_m - nu * pre_GD_m ) # useful for the boundary condition (if present) - # A_m = pre_A_m @ cP1_m + gamma_h * JP_m print(" Reduce time step to match the simulation final time:") Nt = int(np.ceil(final_time / dt)) @@ -447,8 +312,7 @@ def solve_td_maxwell_pbm(*, print(f" . Nb of time steps: Nt = {Nt}") # ... - def is_plotting_time(nt, *, dt=dt, Nt=Nt, - plot_time_ranges=plot_time_ranges): + def is_plotting_time(nt, *, dt=dt, Nt=Nt, plot_time_ranges=plot_time_ranges): if nt in [0, Nt]: return True for [start, end], dt_plots in plot_time_ranges: @@ -459,21 +323,11 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, return False # ... - # Number of time step between two successive calculations of the scalar - # diagnostics - diag_nt = max(int(diag_dt // dt), 1) print(' ------ ------ ------ ------ ------ ------ ------ ------ ') print(' ------ ------ ------ ------ ------ ------ ------ ------ ') - print( - ' total nb of time steps: Nt = {}, final time: T = {:5.4f}'.format( - Nt, - final_time)) + print(' total nb of time steps: Nt = {}, final time: T = {:5.4f}'.format(Nt, final_time)) print(' ------ ------ ------ ------ ------ ------ ------ ------ ') - print(' plotting times: the solution will be plotted for...') - for nt in range(Nt + 1): - if is_plotting_time(nt): - print(' * nt = {}, t = {:5.4f}'.format(nt, dt * nt)) print(' ------ ------ ------ ------ ------ ------ ------ ------ ') print(' ------ ------ ------ ------ ------ ------ ------ ------ ') @@ -483,8 +337,10 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, t_stamp = time_count(t_stamp) print() print(' -- getting source --') - f0_c = None - f0_harmonic_c = None + f0_h = None + f0_harmonic_h = None + rho0_h = None + if source_type == 'zero': f0 = None @@ -492,11 +348,11 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, elif source_type == 'pulse': - f0 = get_div_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + f0 = get_div_free_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) elif source_type == 'cf_pulse': - f0 = get_curl_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + f0 = get_curl_free_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) elif source_type == 'Il_pulse': # Issautier-like pulse # source will be @@ -507,91 +363,60 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, # rho = - sin(om*t)/om * Delta phi # and Gauss' law reads # div E = rho = - sin(om*t)/om * Delta phi - f0 = get_div_free_pulse( - x_0=1.0, y_0=1.0, domain=domain) # this is curl A - f0_harmonic = get_curl_free_pulse( - x_0=1.0, y_0=1.0, domain=domain) # this is grad phi - assert not source_is_harmonic - - rho0 = get_Delta_phi_pulse( - x_0=1.0, y_0=1.0, domain=domain) # this is Delta phi - tilde_rho0_c = derham_h.get_dual_dofs( - space='V0', - f=rho0, - backend_language=backend, - return_format='numpy_array') - tilde_rho0_c = cP0_m.transpose() @ tilde_rho0_c - rho0_c = dH0_m.dot(tilde_rho0_c) + f0 = get_div_free_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) # this is curl A + f0_harmonic = get_curl_free_pulse( x_0=np.pi/2, y_0=np.pi/2, domain=domain) # this is grad phi + + rho0 = get_Delta_phi_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) # this is Delta phi + tilde_rho0_h = get_dual_dofs(Vh=V0h, f=rho0, domain_h=domain_h, backend_language=backend) + tilde_rho0_h = cP0.T @ tilde_rho0_h + rho0_h = dH0.dot(tilde_rho0_h) else: - f0, u_bc, u_ex, curl_u_ex, div_u_ex = get_source_and_solution_hcurl( - source_type=source_type, domain=domain, domain_name=domain_name, - ) + f0, u_bc, u_ex, curl_u_ex, div_u_ex = get_source_and_solution_hcurl(source_type=source_type, domain=domain, domain_name=domain_name) assert u_bc is None # only homogeneous BC's for now - # f0_c = np.zeros(V1h.nbasis) if source_omega is not None: f0_harmonic = f0 f0 = None - if E0_type == 'th_sol': - # use source enveloppe for smooth transition from 0 to 1 - def source_enveloppe(tau): - return (special.erf((tau / 25) - 2) - special.erf(-2)) / 2 - else: - def source_enveloppe(tau): - return 1 + + def source_enveloppe(tau): + return 1 t_stamp = time_count(t_stamp) - tilde_f0_c = f0_c = None - tilde_f0_harmonic_c = f0_harmonic_c = None + tilde_f0_h = f0_h = None + tilde_f0_harmonic_h = f0_harmonic_h = None + if source_proj == 'P_geom': print(' .. projecting the source with commuting projection...') + if f0 is not None: - f0_h = P1_phys(f0, P1, domain, mappings_list) - f0_c = f0_h.coeffs.toarray() - tilde_f0_c = H1_m.dot(f0_c) + f0_h = P1_phys(f0, P1, domain).coeffs + tilde_f0_h = H1.dot(f0_h) + if f0_harmonic is not None: - f0_harmonic_h = P1_phys(f0_harmonic, P1, domain, mappings_list) - f0_harmonic_c = f0_harmonic_h.coeffs.toarray() - tilde_f0_harmonic_c = H1_m.dot(f0_harmonic_c) + f0_harmonic_h = P1_phys(f0_harmonic, P1, domain).coeffs + tilde_f0_harmonic_h = H1.dot(f0_harmonic_h) elif source_proj == 'P_L2': - # helper: save/load coefs + if f0 is not None: if source_type == 'Il_pulse': source_name = 'Il_pulse_f0' else: source_name = source_type - sdd_filename = m_load_dir + '/' + source_name + \ - '_dual_dofs_qp{}.npy'.format(quad_param) - if os.path.exists(sdd_filename): - print( - ' .. loading source dual dofs from file {}'.format(sdd_filename)) - tilde_f0_c = np.load(sdd_filename) - else: - print(' .. projecting the source f0 with L2 projection...') - tilde_f0_c = derham_h.get_dual_dofs( - space='V1', f=f0, backend_language=backend, return_format='numpy_array') - print(' .. saving source dual dofs to file {}'.format(sdd_filename)) - np.save(sdd_filename, tilde_f0_c) + + print(' .. projecting the source f0 with L2 projection...') + tilde_f0_h = get_dual_dofs(Vh=V1h, f=f0, domain_h=domain_h, backend_language=backend) + if f0_harmonic is not None: if source_type == 'Il_pulse': source_name = 'Il_pulse_f0_harmonic' else: source_name = source_type - sdd_filename = m_load_dir + '/' + source_name + \ - '_dual_dofs_qp{}.npy'.format(quad_param) - if os.path.exists(sdd_filename): - print( - ' .. loading source dual dofs from file {}'.format(sdd_filename)) - tilde_f0_harmonic_c = np.load(sdd_filename) - else: - print(' .. projecting the source f0_harmonic with L2 projection...') - tilde_f0_harmonic_c = derham_h.get_dual_dofs( - space='V1', f=f0_harmonic, backend_language=backend, return_format='numpy_array') - print(' .. saving source dual dofs to file {}'.format(sdd_filename)) - np.save(sdd_filename, tilde_f0_harmonic_c) + + print(' .. projecting the source f0_harmonic with L2 projection...') + tilde_f0_harmonic_h = get_dual_dofs(Vh=V1h, f=f0_harmonic, domain_h=domain_h, backend_language=backend) else: raise ValueError(source_proj) @@ -599,259 +424,23 @@ def source_enveloppe(tau): t_stamp = time_count(t_stamp) if filter_source: print(' .. filtering the source...') - if tilde_f0_c is not None: - tilde_f0_c = cP1_m.transpose() @ tilde_f0_c - if tilde_f0_harmonic_c is not None: - tilde_f0_harmonic_c = cP1_m.transpose() @ tilde_f0_harmonic_c - - if tilde_f0_c is not None: - f0_c = dH1_m.dot(tilde_f0_c) - - if debug: - title = 'f0 part of source' - params_str = 'omega={}_gamma_h={}_Pf={}'.format( - source_omega, gamma_h, source_proj) - plot_field(numpy_coeffs=f0_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + '_f0.pdf', - plot_type='components', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) - plot_field(numpy_coeffs=f0_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + '_f0_vf.pdf', - plot_type='vector_field', cb_min=None, cb_max=None, hide_plot=hide_plots) - divf0_c = div_m @ f0_c - title = 'div f0' - plot_field(numpy_coeffs=divf0_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + '_divf0.pdf', - plot_type='components', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) - - if tilde_f0_harmonic_c is not None: - f0_harmonic_c = dH1_m.dot(tilde_f0_harmonic_c) - - if debug: - title = 'f0_harmonic part of source' - params_str = 'omega={}_gamma_h={}_Pf={}'.format( - source_omega, gamma_h, source_proj) - plot_field(numpy_coeffs=f0_harmonic_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + '_f0_harmonic.pdf', - plot_type='components', cb_min=None, cb_max=None, hide_plot=hide_plots) - plot_field(numpy_coeffs=f0_harmonic_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + '_f0_harmonic_vf.pdf', - plot_type='vector_field', cb_min=None, cb_max=None, hide_plot=hide_plots) - divf0_c = div_m @ f0_harmonic_c - title = 'div f0_harmonic' - plot_field(numpy_coeffs=divf0_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + '_divf0_harmonic.pdf', - plot_type='components', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) - - # else: - # raise NotImplementedError - - if f0_c is None: - f0_c = np.zeros(V1h.nbasis) - - # if plot_source and plot_dir: - # plot_field(numpy_coeffs=f0_c, Vh=V1h, space_kind='hcurl', domain=domain, title='f0_h with P = '+source_proj, filename=plot_dir+'/f0h_'+source_proj+'.png', hide_plot=hide_plots) - # plot_field(numpy_coeffs=f0_c, Vh=V1h, plot_type='vector_field', space_kind='hcurl', domain=domain, title='f0_h with P = '+source_proj, filename=plot_dir+'/f0h_'+source_proj+'_vf.png', hide_plot=hide_plots) + if tilde_f0_h is not None: + tilde_f0_h = cP1.T @ tilde_f0_h - t_stamp = time_count(t_stamp) + if tilde_f0_harmonic_h is not None: + tilde_f0_harmonic_h = cP1.T @ tilde_f0_harmonic_h + + if tilde_f0_h is not None: + f0_h = dH1.dot(tilde_f0_h) + + if tilde_f0_harmonic_h is not None: + f0_harmonic_h = dH1.dot(tilde_f0_harmonic_h) - def plot_J_source_nPlusHalf(f_c, nt): - print(' .. plotting the source...') - title = r'source $J^{n+1/2}_h$ (amplitude)' + \ - ' for $\\omega = {}$, $n = {}$'.format(source_omega, nt) - params_str = 'omega={}_gamma_h={}_Pf={}'.format( - source_omega, gamma_h, source_proj) - plot_field(numpy_coeffs=f_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + - '_Jh_nt={}.pdf'.format(nt), - plot_type='amplitude', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) - title = r'source $J^{n+1/2}_h$' + \ - ' for $\\omega = {}$, $n = {}$'.format(source_omega, nt) - plot_field(numpy_coeffs=f_c, Vh=V1h, space_kind='hcurl', domain=domain, title=title, - filename=plot_dir + '/' + params_str + - '_Jh_vf_nt={}.pdf'.format(nt), - plot_type='vector_field', vf_skip=1, hide_plot=hide_plots) - - def plot_E_field(E_c, nt, project_sol=False, plot_divE=False): - - # only E for now - if plot_dir: - - plot_omega_normalized_sol = (source_omega is not None) - # project the homogeneous solution on the conforming problem space - if project_sol: - # t_stamp = time_count(t_stamp) - print( - ' .. projecting the homogeneous solution on the conforming problem space...') - Ep_c = cP1_m.dot(E_c) - else: - Ep_c = E_c - print( - ' .. NOT projecting the homogeneous solution on the conforming problem space') - if plot_omega_normalized_sol: - print(' .. plotting the E/omega field...') - u_c = (1 / source_omega) * Ep_c - title = r'$u_h = E_h/\omega$ (amplitude) for $\omega = {:5.4f}$, $t = {:5.4f}$'.format( - source_omega, dt * nt) - params_str = 'omega={:5.4f}_gamma_h={}_Pf={}_Nt_pp={}'.format( - source_omega, gamma_h, source_proj, Nt_pp) - else: - print(' .. plotting the E field...') - if E0_type == 'pulse': - title = r'$t = {:5.4f}$'.format(dt * nt) - else: - title = r'$E_h$ (amplitude) at $t = {:5.4f}$'.format( - dt * nt) - u_c = Ep_c - params_str = f'gamma_h={gamma_h}_dt={dt}' - - plot_field(numpy_coeffs=u_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + - '_Eh_nt={}.pdf'.format(nt), - plot_type='amplitude', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) - - if plot_divE: - params_str = f'gamma_h={gamma_h}_dt={dt}' - if source_type == 'Il_pulse': - plot_type = 'components' - rho_c = rho0_c * \ - np.sin(source_omega * dt * nt) / source_omega - rho_norm2 = np.dot(rho_c, H0_m.dot(rho_c)) - title = r'$\rho_h$ at $t = {:5.4f}, norm = {}$'.format( - dt * nt, np.sqrt(rho_norm2)) - plot_field(numpy_coeffs=rho_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + - '_rho_nt={}.pdf'.format(nt), - plot_type=plot_type, cb_min=None, cb_max=None, hide_plot=hide_plots) - else: - plot_type = 'amplitude' - - divE_c = div_m @ Ep_c - divE_norm2 = np.dot(divE_c, H0_m.dot(divE_c)) - if project_sol: - title = r'div $P^1_h E_h$ at $t = {:5.4f}, norm = {}$'.format( - dt * nt, np.sqrt(divE_norm2)) - else: - title = r'div $E_h$ at $t = {:5.4f}, norm = {}$'.format( - dt * nt, np.sqrt(divE_norm2)) - plot_field(numpy_coeffs=divE_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + - '_divEh_nt={}.pdf'.format(nt), - plot_type=plot_type, cb_min=None, cb_max=None, hide_plot=hide_plots) - - else: - print(' -- WARNING: unknown plot_dir !!') - - def plot_B_field(B_c, nt): - - if plot_dir: - - print(' .. plotting B field...') - params_str = f'gamma_h={gamma_h}_dt={dt}' - - title = r'$B_h$ (amplitude) for $t = {:5.4f}$'.format(dt * nt) - plot_field(numpy_coeffs=B_c, Vh=V2h, space_kind='l2', domain=domain, surface_plot=False, title=title, - filename=plot_dir + '/' + params_str + - '_Bh_nt={}.pdf'.format(nt), - plot_type='amplitude', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) - - else: - print(' -- WARNING: unknown plot_dir !!') - - def plot_time_diags(time_diag, E_norm2_diag, B_norm2_diag, divE_norm2_diag, nt_start, nt_end, - GaussErr_norm2_diag=None, GaussErrP_norm2_diag=None, - PE_norm2_diag=None, I_PE_norm2_diag=None, J_norm2_diag=None, skip_titles=True): - - nt_start = max(nt_start, 0) - nt_end = min(nt_end, Nt) - - td = time_diag[nt_start:nt_end + 1] - t_label = r'$t$' - - # norm || E || - fig, ax = plt.subplots() - ax.plot(td, - np.sqrt(E_norm2_diag[nt_start:nt_end + 1]), - '-', - ms=7, - mfc='None', - mec='k') # , label='||E||', zorder=10) - if skip_titles: - title = '' - else: - title = r'$||E_h(t)||$ vs ' + t_label - ax.set_xlabel(t_label, fontsize=16) - ax.set_title(title, fontsize=18) - fig.tight_layout() - diag_fn = plot_dir + \ - f'/diag_E_norm_gamma={gamma_h}_dt={dt}_trange=[{dt*nt_start}, {dt*nt_end}].pdf' - print(f"saving plot for '{title}' in figure '{diag_fn}") - fig.savefig(diag_fn) - - # energy - fig, ax = plt.subplots() - E_energ = .5 * E_norm2_diag[nt_start:nt_end + 1] - B_energ = .5 * B_norm2_diag[nt_start:nt_end + 1] - ax.plot(td, E_energ, '-', ms=7, mfc='None', c='k', - label=r'$\frac{1}{2}||E||^2$') # , zorder=10) - ax.plot(td, B_energ, '-', ms=7, mfc='None', c='g', - label=r'$\frac{1}{2}||B||^2$') # , zorder=10) - ax.plot(td, E_energ + B_energ, '-', ms=7, mfc='None', c='b', - label=r'$\frac{1}{2}(||E||^2+||B||^2)$') # , zorder=10) - ax.legend(loc='best') - if skip_titles: - title = '' - else: - title = r'energy vs ' + t_label - if E0_type == 'pulse': - ax.set_ylim([0, 5]) - ax.set_xlabel(t_label, fontsize=16) - ax.set_title(title, fontsize=18) - fig.tight_layout() - diag_fn = plot_dir + \ - f'/diag_energy_gamma={gamma_h}_dt={dt}_trange=[{dt*nt_start},{dt*nt_end}].pdf' - print(f"saving plot for '{title}' in figure '{diag_fn}") - fig.savefig(diag_fn) - - # One curve per plot from now on. - # Collect information in a list where each item is of the form [tag, - # data, title] - time_diagnostics = [] - - if project_sol: - time_diagnostics += [['divPE', divE_norm2_diag, - r'$||div_h P^1_h E_h(t)||$ vs ' + t_label]] - else: - time_diagnostics += [['divE', divE_norm2_diag, - r'$||div_h E_h(t)||$ vs ' + t_label]] - - time_diagnostics += [ - ['I_PE', I_PE_norm2_diag, r'$||(I-P^1)E_h(t)||$ vs ' + t_label], - ['PE', PE_norm2_diag, r'$||(I-P^1)E_h(t)||$ vs ' + t_label], - ['GaussErr', GaussErr_norm2_diag, - r'$||(\rho_h - div_h E_h)(t)||$ vs ' + t_label], - ['GaussErrP', GaussErrP_norm2_diag, - r'$||(\rho_h - div_h E_h)(t)||$ vs ' + t_label], - ['J_norm', J_norm2_diag, r'$||J_h(t)||$ vs ' + t_label], - ] - for tag, data, title in time_diagnostics: - if data is None: - continue - fig, ax = plt.subplots() - ax.plot(td, - np.sqrt(I_PE_norm2_diag[nt_start:nt_end + 1]), - '-', - ms=7, - mfc='None', - mec='k') # , label='||E||', zorder=10) - diag_fn = plot_dir + \ - f'/diag_{tag}_gamma={gamma_h}_dt={dt}_trange=[{dt*nt_start},{dt*nt_end}].pdf' - ax.set_xlabel(t_label, fontsize=16) - if not skip_titles: - ax.set_title(title, fontsize=18) - fig.tight_layout() - print(f"saving plot for '{title}' in figure '{diag_fn}") - fig.savefig(diag_fn) + if f0_h is None: + f0_h = V1h.coeff_space.zeros() + + t_stamp = time_count(t_stamp) # diags arrays E_norm2_diag = np.zeros(Nt + 1) @@ -874,114 +463,73 @@ def plot_time_diags(time_diag, E_norm2_diag, B_norm2_diag, divE_norm2_diag, nt_s print(' .. initial solution ..') # initial B sol - B_c = np.zeros(V2h.nbasis) + B_h = V2h.coeff_space.zeros() + E_h = V1h.coeff_space.zeros() # initial E sol - if E0_type == 'th_sol': - - if os.path.exists(th_sol_filename): - print( - ' .. loading time-harmonic solution from file {}'.format(th_sol_filename)) - E_c = source_omega * np.load(th_sol_filename) - assert len(E_c) == V1h.nbasis - else: - print( - ' .. Error: time-harmonic solution file given {}, but not found'.format(th_sol_filename)) - raise ValueError(th_sol_filename) - - elif E0_type == 'zero': - E_c = np.zeros(V1h.nbasis) + if E0_type == 'zero': + E_h = V1h.coeff_space.zeros() elif E0_type == 'pulse': - E0 = get_div_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + E0 = get_div_free_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) if E0_proj == 'P_geom': print(' .. projecting E0 with commuting projection...') - E0_h = P1_phys(E0, P1, domain, mappings_list) - E_c = E0_h.coeffs.toarray() + E0_h = P1_phys(E0, P1, domain) + E_h = E0_h.coeffs elif E0_proj == 'P_L2': - # helper: save/load coefs - E0dd_filename = m_load_dir + \ - '/E0_pulse_dual_dofs_qp{}.npy'.format(quad_param) - if os.path.exists(E0dd_filename): - print(' .. loading E0 dual dofs from file {}'.format(E0dd_filename)) - tilde_E0_c = np.load(E0dd_filename) - else: - print(' .. projecting E0 with L2 projection...') - tilde_E0_c = derham_h.get_dual_dofs( - space='V1', f=E0, backend_language=backend, return_format='numpy_array') - print(' .. saving E0 dual dofs to file {}'.format(E0dd_filename)) - np.save(E0dd_filename, tilde_E0_c) - E_c = dH1_m.dot(tilde_E0_c) - elif E0_type == 'pulse_2': - # E0 = get_praxial_Gaussian_beam_E(x_0=3.14, y_0=3.14, domain=domain) + print(' .. projecting E0 with L2 projection...') + tilde_E0_h = get_dual_dofs(Vh=V1h, f=E0, domain_h=domain_h, backend_language=backend) + E_h = dH1.dot(tilde_E0_h) - # E0 = get_easy_Gaussian_beam_E_2(x_0=0.05, y_0=0.05, domain=domain) - # B0 = get_easy_Gaussian_beam_B_2(x_0=0.05, y_0=0.05, domain=domain) + elif E0_type == 'pulse_2': - E0, B0 = get_Gaussian_beam(y_0=3.14, x_0=3.14, domain=domain) - # B0 = get_easy_Gaussian_beam_B(x_0=3.14, y_0=0.05, domain=domain) + E0, B0 = get_Gaussian_beam(y_0=np.pi/2, x_0=np.pi/2, domain=domain) if E0_proj == 'P_geom': print(' .. projecting E0 with commuting projection...') - E0_h = P1_phys(E0, P1, domain, mappings_list) - E_c = E0_h.coeffs.toarray() + E0_h = P1_phys(E0, P1, domain) + E_h = E0_h.coeffs - # B_c = np.real( - 1j * C_m @ E_c) - # E_c = np.real(E_c) - B0_h = P2_phys(B0, P2, domain, mappings_list) - B_c = B0_h.coeffs.toarray() + B0_h = P2_phys(B0, P2, domain) + B_h = B0_h.coeffs elif E0_proj == 'P_L2': - # helper: save/load coefs - E0dd_filename = m_load_dir + \ - '/E0_pulse_dual_dofs_qp{}.npy'.format(quad_param) - if False: # os.path.exists(E0dd_filename): - print(' .. loading E0 dual dofs from file {}'.format(E0dd_filename)) - tilde_E0_c = np.load(E0dd_filename) - else: - print(' .. projecting E0 with L2 projection...') - - tilde_E0_c = derham_h.get_dual_dofs( - space='V1', f=E0, backend_language=backend, return_format='numpy_array') - print(' .. saving E0 dual dofs to file {}'.format(E0dd_filename)) - # np.save(E0dd_filename, tilde_E0_c) + + print(' .. projecting E0 with L2 projection...') + tilde_E0_h = get_dual_dofs(Vh=V1h, f=E0, domain_h=domain_h, backend_language=backend) + E_h = dH1.dot(tilde_E0_h) - E_c = dH1_m.dot(tilde_E0_c) - dH2_m = H2.get_dual_sparse_matrix() - tilde_B0_c = derham_h.get_dual_dofs( - space='V2', f=B0, backend_language=backend, return_format='numpy_array') - B_c = dH2_m.dot(tilde_B0_c) + tilde_B0_h = get_dual_dofs(Vh=V2h, f=B0, domain_h=domain_h, backend_language=backend) + B_h = dH2.dot(tilde_B0_h) - # B_c = np.real( - C_m @ E_c) - # E_c = np.real(E_c) else: raise ValueError(E0_type) # ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- # time loop - def compute_diags(E_c, B_c, J_c, nt): + def compute_diags(E_h, B_h, J_h, nt): time_diag[nt] = (nt) * dt - PE_c = cP1_m.dot(E_c) - I_PE_c = E_c - PE_c - E_norm2_diag[nt] = np.dot(E_c, H1_m.dot(E_c)) - PE_norm2_diag[nt] = np.dot(PE_c, H1_m.dot(PE_c)) - I_PE_norm2_diag[nt] = np.dot(I_PE_c, H1_m.dot(I_PE_c)) - J_norm2_diag[nt] = np.dot(J_c, H1_m.dot(J_c)) - B_norm2_diag[nt] = np.dot(B_c, H2_m.dot(B_c)) - divE_c = div_m @ E_c - divE_norm2_diag[nt] = np.dot(divE_c, H0_m.dot(divE_c)) - if source_type == 'Il_pulse': - rho_c = rho0_c * np.sin(source_omega * nt * dt) / omega - GaussErr = rho_c - divE_c - GaussErrP = rho_c - div_m @ PE_c - GaussErr_norm2_diag[nt] = np.dot(GaussErr, H0_m.dot(GaussErr)) - GaussErrP_norm2_diag[nt] = np.dot(GaussErrP, H0_m.dot(GaussErrP)) + PE_h = cP1.dot(E_h) + I_PE_h = E_h - PE_h + E_norm2_diag[nt] = E_h.inner(H1.dot(E_h)) + PE_norm2_diag[nt] = PE_h.inner(H1.dot(PE_h)) + I_PE_norm2_diag[nt] = I_PE_h.inner(H1.dot(I_PE_h)) + J_norm2_diag[nt] = J_h.inner(H1.dot(J_h)) + B_norm2_diag[nt] = B_h.inner(H2.dot(B_h)) + divE_h = D @ E_h + divE_norm2_diag[nt] = divE_h.inner(H0.dot(divE_h)) + if source_type == 'Il_pulse' and source_omega is not None: + rho_h = rho0_h * np.sin(source_omega * nt * dt) / omega + GaussErr = rho_h - divE_h + GaussErrP = rho_h - D @ PE_h + GaussErr_norm2_diag[nt] = GaussErr.inner(H0.dot(GaussErr)) + GaussErrP_norm2_diag[nt] = GaussErrP.inner(H0.dot(GaussErrP)) if plot_dir: OM1 = OutputManager(plot_dir + '/spaces1.yml', plot_dir + '/fields1.h5') @@ -992,206 +540,85 @@ def compute_diags(E_c, B_c, J_c, nt): OM2.add_spaces(V2h=V2h) OM2.export_space_info() - stencil_coeffs_E = array_to_psydac(cP1_m @ E_c, V1h.coeff_space) - Eh = FemField(V1h, coeffs=stencil_coeffs_E) + Eh = FemField(V1h, coeffs=cP1 @ E_h) OM1.add_snapshot(t=0, ts=0) OM1.export_fields(Eh=Eh) - stencil_coeffs_B = array_to_psydac(B_c, V2h.coeff_space) - Bh = FemField(V2h, coeffs=stencil_coeffs_B) + Bh = FemField(V2h, coeffs=B_h) OM2.add_snapshot(t=0, ts=0) OM2.export_fields(Bh=Bh) - # PM = PostProcessManager(domain=domain, space_file=plot_dir+'/spaces1.yml', fields_file=plot_dir+'/fields1.h5' ) - # PM.export_to_vtk(plot_dir+"/Eh",grid=None, npts_per_cell=[6]*2, snapshots='all', fields='vh' ) - - # OM1.close() - # PM.close() - - # plot_E_field(E_c, nt=0, project_sol=project_sol, plot_divE=plot_divE) - # plot_B_field(B_c, nt=0) - f_c = np.copy(f0_c) + f_h = f0_h.copy() for nt in range(Nt): print(' .. nt+1 = {}/{}'.format(nt + 1, Nt)) # 1/2 faraday: Bn -> Bn+1/2 - B_c[:] -= (dt / 2) * C_m @ E_c + B_h -= (dt / 2) * C @ E_h # ampere: En -> En+1 - if f0_harmonic_c is not None: - f_harmonic_c = f0_harmonic_c * (np.sin(source_omega * (nt + 1) * dt) - np.sin( - source_omega * (nt) * dt)) / (dt * source_omega) # * source_enveloppe(omega*(nt+1/2)*dt) - f_c[:] = f0_c + f_harmonic_c + if f0_harmonic_h is not None and source_omega is not None: + f_harmonic_h = f0_harmonic_h * (np.sin(source_omega * (nt + 1) * dt) - np.sin(source_omega * (nt) * dt)) / (dt * source_omega) # * source_enveloppe(omega*(nt+1/2)*dt) + f_h = f0_h + f_harmonic_h - if nt == 0: - if plot_dir: - plot_J_source_nPlusHalf(f_c, nt=0) - compute_diags(E_c, B_c, f_c, nt=0) - - E_c[:] = dCH1_m @ E_c + dt * (dC_m @ B_c - f_c) - - # if abs(gamma_h) > 1e-10: - # E_c[:] -= dt * gamma_h * JP_m @ E_c + E_h = dCH1 @ E_h + dt * (dC @ B_h - f_h) # 1/2 faraday: Bn+1/2 -> Bn+1 - B_c[:] -= (dt / 2) * C_m @ E_c + B_h -= (dt / 2) * C @ E_h # diags: - compute_diags(E_c, B_c, f_c, nt=nt + 1) - - # PE_c = cP1_m.dot(E_c) - # I_PE_c = E_c-PE_c - # E_norm2_diag[nt+1] = np.dot(E_c,H1_m.dot(E_c)) - # PE_norm2_diag[nt+1] = np.dot(PE_c,H1_m.dot(PE_c)) - # I_PE_norm2_diag[nt+1] = np.dot(I_PE_c,H1_m.dot(I_PE_c)) - # B_norm2_diag[nt+1] = np.dot(B_c,H2_m.dot(B_c)) - # time_diag[nt+1] = (nt+1)*dt - - # diags: div - # if project_sol: - # Ep_c = PE_c # = cP1_m.dot(E_c) - # else: - # Ep_c = E_c - # divE_c = div_m @ Ep_c - # divE_norm2 = np.dot(divE_c, H0_m.dot(divE_c)) - # # print('in diag[{}]: divE_norm = {}'.format(nt+1, np.sqrt(divE_norm2))) - # divE_norm2_diag[nt+1] = divE_norm2 - - # if source_type == 'Il_pulse': - # rho_c = rho0_c * np.sin(omega*dt*(nt+1))/omega - # GaussErr = rho_c - div_m @ E_c - # GaussErrP = rho_c - div_m @ (cP1_m.dot(E_c)) - # GaussErr_norm2_diag[nt+1] = np.dot(GaussErr, H0_m.dot(GaussErr)) - # GaussErrP_norm2_diag[nt+1] = np.dot(GaussErrP, H0_m.dot(GaussErrP)) - - if debug: - divCB_c = div_m @ dC_m @ B_c - divCB_norm2 = np.dot(divCB_c, H0_m.dot(divCB_c)) - print('-- [{}]: dt*|| div CB || = {}'.format(nt + - 1, dt * np.sqrt(divCB_norm2))) - - divf_c = div_m @ f_c - divf_norm2 = np.dot(divf_c, H0_m.dot(divf_c)) - print('-- [{}]: dt*|| div f || = {}'.format(nt + - 1, dt * np.sqrt(divf_norm2))) - - divE_c = div_m @ E_c - divE_norm2 = np.dot(divE_c, H0_m.dot(divE_c)) - print('-- [{}]: || div E || = {}'.format(nt + 1, np.sqrt(divE_norm2))) + compute_diags(E_h, B_h, f_h, nt=nt + 1) + + if is_plotting_time(nt + 1) and plot_dir: - print("Plot Stuff") - # plot_E_field(E_c, nt=nt+1, project_sol=True, plot_divE=False) - # plot_B_field(B_c, nt=nt+1) - # plot_J_source_nPlusHalf(f_c, nt=nt) + print("Plot fields") - stencil_coeffs_E = array_to_psydac(cP1_m @ E_c, V1h.coeff_space) - Eh = FemField(V1h, coeffs=stencil_coeffs_E) + Eh = FemField(V1h, coeffs=cP1 @ E_h) OM1.add_snapshot(t=nt * dt, ts=nt) OM1.export_fields(Eh=Eh) - stencil_coeffs_B = array_to_psydac(B_c, V2h.coeff_space) - Bh = FemField(V2h, coeffs=stencil_coeffs_B) + Bh = FemField(V2h, coeffs=B_h) OM2.add_snapshot(t=nt * dt, ts=nt) OM2.export_fields(Bh=Bh) - # if (nt+1) % diag_nt == 0: - # plot_time_diags(time_diag, E_norm2_diag, B_norm2_diag, divE_norm2_diag, nt_start=(nt+1)-diag_nt, nt_end=(nt+1), - # PE_norm2_diag=PE_norm2_diag, I_PE_norm2_diag=I_PE_norm2_diag, J_norm2_diag=J_norm2_diag, - # GaussErr_norm2_diag=GaussErr_norm2_diag, - # GaussErrP_norm2_diag=GaussErrP_norm2_diag) + if plot_dir: OM1.close() - print("Do some PP") + print("Post process fields") PM = PostProcessManager( domain=domain, - space_file=plot_dir + - '/spaces1.yml', - fields_file=plot_dir + - '/fields1.h5') + space_file=plot_dir + '/spaces1.yml', + fields_file=plot_dir + '/fields1.h5') PM.export_to_vtk( plot_dir + "/Eh", grid=None, - npts_per_cell=2, + npts_per_cell=4, snapshots='all', fields='Eh') PM.close() PM = PostProcessManager( domain=domain, - space_file=plot_dir + - '/spaces2.yml', - fields_file=plot_dir + - '/fields2.h5') + space_file=plot_dir + '/spaces2.yml', + fields_file=plot_dir + '/fields2.h5') PM.export_to_vtk( plot_dir + "/Bh", grid=None, - npts_per_cell=2, + npts_per_cell=4, snapshots='all', fields='Bh') PM.close() - # plot_time_diags(time_diag, E_norm2_diag, B_norm2_diag, divE_norm2_diag, nt_start=0, nt_end=Nt, - # PE_norm2_diag=PE_norm2_diag, I_PE_norm2_diag=I_PE_norm2_diag, J_norm2_diag=J_norm2_diag, - # GaussErr_norm2_diag=GaussErr_norm2_diag, - # GaussErrP_norm2_diag=GaussErrP_norm2_diag) - - # Eh = FemField(V1h, coeffs=array_to_stencil(E_c, V1h.coeff_space)) - # t_stamp = time_count(t_stamp) - - # if sol_filename: - # raise NotImplementedError - # print(' .. saving final solution coeffs to file {}'.format(sol_filename)) - # np.save(sol_filename, E_c) - - # time_count(t_stamp) - - # print() - # print(' -- plots and diagnostics --') - - # # diagnostics: errors - # err_diags = diag_grid.get_diags_for(v=uh, space='V1') - # for key, value in err_diags.items(): - # diags[key] = value - # if u_ex is not None: - # check_diags = get_Vh_diags_for(v=uh, v_ref=uh_ref, M_m=H1_m, msg='error between Ph(u_ex) and u_h') - # diags['norm_Pu_ex'] = check_diags['sol_ref_norm'] - # diags['rel_l2_error_in_Vh'] = check_diags['rel_l2_error'] - # if curl_u_ex is not None: - # print(' .. diag on curl_u:') - # curl_uh_c = bD1_m @ cP1_m @ uh_c - # title = r'curl $u_h$ (amplitude) for $\eta = $'+repr(eta) - # params_str = 'eta={}_mu={}_nu={}_gamma_h={}_Pf={}'.format(eta, mu, nu, gamma_h, source_proj) - # plot_field(numpy_coeffs=curl_uh_c, Vh=V2h, space_kind='l2', domain=domain, surface_plot=False, title=title, filename=plot_dir+'/'+params_str+'_curl_uh.png', - # plot_type='amplitude', cb_min=None, cb_max=None, hide_plot=hide_plots) - - # curl_uh = FemField(V2h, coeffs=array_to_stencil(curl_uh_c, V2h.coeff_space)) - # curl_diags = diag_grid.get_diags_for(v=curl_uh, space='V2') - # diags['curl_error (to be checked)'] = curl_diags['rel_l2_error'] - - # title = r'div_h $u_h$ (amplitude) for $\eta = $'+repr(eta) - # params_str = 'eta={}_mu={}_nu={}_gamma_h={}_Pf={}'.format(eta, mu, nu, gamma_h, source_proj) - # plot_field(numpy_coeffs=div_uh_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, filename=plot_dir+'/'+params_str+'_div_uh.png', - # plot_type='amplitude', cb_min=None, cb_max=None, hide_plot=hide_plots) - - # div_uh = FemField(V0h, coeffs=array_to_stencil(div_uh_c, V0h.coeff_space)) - # div_diags = diag_grid.get_diags_for(v=div_uh, space='V0') - # diags['div_error (to be checked)'] = div_diags['rel_l2_error'] - - return diags - - -# def compute_stable_dt(cfl_max, dt_max, C_m, dC_m, V1_dim): -def compute_stable_dt(*, C_m, dC_m, cfl_max, dt_max=None): +def compute_stable_dt(*, C, dC, cfl_max, dt_max=None): """ Compute a stable time step size based on the maximum CFL parameter in the domain. To this end we estimate the operator norm of - `dC_m @ C_m: V1h -> V1h`, + `dC @ C: V1h -> V1h`, find the largest stable time step compatible with Strang splitting, and rescale it by the provided `cfl_max`. Setting `cfl_max = 1` would run the @@ -1204,10 +631,10 @@ def compute_stable_dt(*, C_m, dC_m, cfl_max, dt_max=None): Parameters ---------- - C_m : scipy.sparse.spmatrix + C : LinearOperator Matrix of the Curl operator. - dC_m : scipy.sparse.spmatrix + dC : LinearOperator Matrix of the dual Curl operator. cfl_max : float @@ -1227,36 +654,37 @@ def compute_stable_dt(*, C_m, dC_m, cfl_max, dt_max=None): print(" .. compute_stable_dt by estimating the operator norm of ") print(" .. dC_m @ C_m: V1h -> V1h ") - print(" .. with dim(V1h) = {} ...".format(C_m.shape[1])) + print(" .. with dim(V1h) = {} ...".format(C.domain.dimension)) if not (0 < cfl_max < 1): print(' ****** ****** ****** ****** ****** ****** ') print(' WARNING !!! cfl = {} '.format(cfl)) print(' ****** ****** ****** ****** ****** ****** ') - def vect_norm_2(vv): - return np.sqrt(np.dot(vv, vv)) - t_stamp = time_count() - vv = np.random.random(C_m.shape[1]) - norm_vv = vect_norm_2(vv) + V = C.domain + from psydac.linalg.utilities import array_to_psydac + vv = array_to_psydac(np.random.rand(V.dimension), V) + + norm_vv = np.sqrt(vv.inner(vv)) + max_ncfl = 500 ncfl = 0 spectral_rho = 1 conv = False - CC_m = dC_m @ C_m + CC = dC @ C while not (conv or ncfl > max_ncfl): - vv[:] = (1. / norm_vv) * vv + vv *= (1. / norm_vv) ncfl += 1 - vv[:] = CC_m.dot(vv) + CC.dot(vv, out=vv) - norm_vv = vect_norm_2(vv) + norm_vv = np.sqrt(vv.inner(vv)) old_spectral_rho = spectral_rho - spectral_rho = vect_norm_2(vv) # approximation + spectral_rho = norm_vv # approximation conv = abs((spectral_rho - old_spectral_rho) / spectral_rho) < 0.001 - print(" ... spectral radius iteration: spectral_rho( dC_m @ C_m ) ~= {}".format(spectral_rho)) + print(" ... spectral radius iteration: spectral_rho( dC @ C ) ~= {}".format(spectral_rho)) t_stamp = time_count(t_stamp) norm_op = np.sqrt(spectral_rho) @@ -1269,11 +697,8 @@ def vect_norm_2(vv): dt = min(dt, dt_max) print(" Time step dt computed for Maxwell solver:") - print( - f" Based on cfl_max = {cfl_max} and dt_max = {dt_max}, we set dt = {dt}") - print( - f" -- note that c*Dt = {light_c*dt} and c_dt_max = {c_dt_max}, thus c * dt / c_dt_max = {light_c*dt/c_dt_max}") - print( - f" -- and spectral_radius((c*dt)**2* dC_m @ C_m ) = {(light_c * dt * norm_op)**2} (should be < 4).") + print(f" Based on cfl_max = {cfl_max} and dt_max = {dt_max}, we set dt = {dt}") + print(f" -- note that c*Dt = {light_c*dt} and c_dt_max = {c_dt_max}, thus c * dt / c_dt_max = {light_c*dt/c_dt_max}") + print(f" -- and spectral_radius((c*dt)**2* dC @ C ) = {(light_c * dt * norm_op)**2} (should be < 4).") return dt diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py b/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py index 19c1e13d6..e17c70b4a 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py @@ -5,22 +5,15 @@ import numpy as np from psydac.feec.multipatch.examples.timedomain_maxwell import solve_td_maxwell_pbm -from psydac.feec.multipatch.utilities import time_count, FEM_sol_fn, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn -from psydac.feec.multipatch.utils_conga_2d import write_diags_to_file - -t_stamp_full = time_count() +from psydac.feec.multipatch.utilities import get_run_dir, get_plot_dir # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- # -# main test-cases and parameters used for the ppc paper: - -test_case = 'E0_pulse_no_source' # used in paper -# test_case = 'Issautier_like_source' # used in paper -# test_case = 'transient_to_harmonic' # actually, not used in paper +test_case = 'E0_pulse_no_source' +# test_case = 'Issautier_like_source' # J_proj_case = 'P_geom' J_proj_case = 'P_L2' -# J_proj_case = 'tilde Pi_1' # # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- @@ -34,62 +27,21 @@ # domain_name = 'pretzel_f' # non-conf domains -domain = [[0, 2 * np.pi], [0, 2 * np.pi]] # interval in x- and y-direction +domain = [[0, np.pi], [0, np.pi]] # interval in x- and y-direction domain_name = 'refined_square' # use isotropic meshes (probably with a square domain) # 4x8= 64 patches # care for the transpose -ncells = np.array([[16, 16], - [16, 16]]) - -# ncells = np.array([[8,8,16,8], -# [8,8,16,8], -# [8,8,16,8], -# [8,8,16,8]]) -# ncells = np.array([[8,8,8,8], -# [8,8,8,8], -# [8,8,8,8], -# [8,8,8,8]]) -# ncells = np.array([[8,8,16,8,8,8], -# [8,8,16,8,8,8], -# [8,8,16,8,8,8], -# [8,8,16,8,8,8]]) - -# ncells = np.array([[4, 4, 4], -# [4, 8, 4], -# [8, 16, 8], -# [4, 8, 4], -# [4, 4, 4]]) -# ncells = np.array([[4, 4, 4, 4], -# [4, 8, 8, 4], -# [8, 16, 16, 8], -# [4, 8, 8, 4], -# [4, 4, 4, 4]]).transpose() -# ncells = np.array([[4, 4, 4, 4], -# [4, 4, 4, 4], -# [4, 8, 8, 4], -# [8, 16, 16, 8], -# [8, 16, 16, 8], -# [4, 8, 8, 4], -# [4, 4, 4, 4], -# [4, 4, 4, 4]]) - +ncells = np.array([[10, 10, 10], + [10, 20, 10], + [10, 10, 10]]) cfl_max = 0.8 + # 'P_geom' # projection used for initial E0 (B0 = 0 in all cases) -E0_proj = 'P_geom' +E0_proj = 'P_L2' backend = 'pyccel-gcc' project_sol = True # whether cP1 E_h is plotted instead of E_h -# multiplicative parameter for quadrature order in (bi)linear forms -# discretizaion -quad_param = 4 -gamma_h = 0 # jump dissipation parameter (not used in paper) -# 'BSP' # type of conforming projection operators (averaging B-spline or Geometric-splines coefficients) -conf_proj = 'GSP' -hide_plots = True -plot_divE = True -# time interval between scalar diagnostics (if None, compute every time step) -diag_dt = None # Parameters that depend on test case if test_case == 'E0_pulse_no_source': @@ -97,9 +49,8 @@ E0_type = 'pulse_2' # non-zero initial conditions source_type = 'zero' # no current source source_omega = None - final_time = 9.02 # wave transit time in domain is > 4 + final_time = 2 # wave transit time in domain is > 4 dt_max = None - plot_source = False plot_a_lot = True if plot_a_lot: @@ -110,9 +61,6 @@ [[final_time - 1, final_time], 0.1], ] - cb_min_sol = 0 - cb_max_sol = 5 - # TODO: check elif test_case == 'Issautier_like_source': @@ -120,9 +68,9 @@ source_type = 'Il_pulse' source_omega = None final_time = 20 - plot_source = True dt_max = None - if deg_s == [3] and final_time == 20: + + if deg == 3 and final_time == 20: plot_time_ranges = [ [[1.9, 2], 0.1], @@ -131,36 +79,6 @@ [[19.9, 20], 0.1], ] - # plot_time_ranges = [ - # ] - # if nc_s == [8]: - # Nt_pp = 10 - - cb_min_sol = 0 # None - cb_max_sol = 0.3 # None - -# TODO: check -elif test_case == 'transient_to_harmonic': - - E0_type = 'th_sol' - source_type = 'elliptic_J' - source_omega = np.sqrt(50) # source time pulsation - plot_source = True - - source_period = 2 * np.pi / source_omega - nb_t_periods = 100 - Nt_pp = 20 - - dt_max = source_period / Nt_pp - final_time = nb_t_periods * source_period - - plot_time_ranges = [ - [[(nb_t_periods - 2) * source_period, final_time], dt_max] - ] - - cb_min_sol = 0 - cb_max_sol = 1 - else: raise ValueError(test_case) @@ -181,8 +99,8 @@ else: raise ValueError(J_proj_case) -case_dir = 'nov14_' + test_case + '_J_proj=' + \ - J_proj_case + '_qp{}'.format(quad_param) +case_dir = 'tdmaxwell_' + test_case + '_J_proj=' + J_proj_case + if filter_source: case_dir += '_Jfilter' else: @@ -198,32 +116,17 @@ # # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -common_diag_filename = './' + case_dir + '_diags.txt' - - run_dir = get_run_dir( domain_name, sum(ncells), deg, source_type=source_type, - conf_proj=conf_proj) + conf_proj="") + plot_dir = get_plot_dir(case_dir, run_dir) -diag_filename = plot_dir + '/' + \ - diag_fn(source_type=source_type, source_proj=source_proj) - -# to save and load matrices -m_load_dir = get_mat_dir(domain_name, sum(ncells), deg, quad_param=quad_param) - -if E0_type == 'th_sol': - # initial E0 will be loaded from time-harmonic FEM solution - th_case_dir = 'maxwell_hom_eta=50' - th_sol_dir = get_sol_dir(th_case_dir, domain_name, sum(ncells), deg) - th_sol_filename = th_sol_dir + '/' + \ - FEM_sol_fn(source_type=source_type, source_proj=source_proj) -else: - # no initial solution to load - th_sol_filename = '' + +# params = { 'nc': ncells, 'deg': deg, @@ -235,23 +138,12 @@ 'source_type': source_type, 'source_omega': source_omega, 'source_proj': source_proj, - 'conf_proj': conf_proj, - 'gamma_h': gamma_h, 'project_sol': project_sol, 'filter_source': filter_source, - 'quad_param': quad_param, 'E0_type': E0_type, 'E0_proj': E0_proj, - 'hide_plots': hide_plots, 'plot_dir': plot_dir, 'plot_time_ranges': plot_time_ranges, - 'plot_source': plot_source, - 'plot_divE': plot_divE, - 'diag_dt': diag_dt, - 'cb_min_sol': cb_min_sol, - 'cb_max_sol': cb_max_sol, - 'm_load_dir': m_load_dir, - 'th_sol_filename': th_sol_filename, 'domain_lims': domain } @@ -259,17 +151,4 @@ print(' Calling solve_td_maxwell_pbm() with params = {}'.format(params)) print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') -diags = solve_td_maxwell_pbm(**params) - -write_diags_to_file( - diags, - script_filename=__file__, - diag_filename=diag_filename, - params=params) -write_diags_to_file( - diags, - script_filename=__file__, - diag_filename=common_diag_filename, - params=params) - -time_count(t_stamp_full, msg='full program') +solve_td_maxwell_pbm(**params) diff --git a/psydac/feec/multipatch/fem_linear_operators.py b/psydac/feec/multipatch/fem_linear_operators.py deleted file mode 100644 index aafb77931..000000000 --- a/psydac/feec/multipatch/fem_linear_operators.py +++ /dev/null @@ -1,218 +0,0 @@ -# coding: utf-8 - -from mpi4py import MPI - -from scipy.sparse import eye as sparse_id - -from psydac.linalg.basic import LinearOperator -from psydac.fem.basic import FemField - -#=============================================================================== -class FemLinearOperator( LinearOperator ): - """ - Linear operators with an additional Fem layer - """ - - def __init__( self, fem_domain=None, fem_codomain=None, matrix=None, sparse_matrix=None): - """ - we may store the matrix of the linear operator with different formats - :param matrix: stencil format - :param sparse_matrix: scipy sparse format - """ - assert fem_domain - self._fem_domain = fem_domain - if fem_codomain: - self._fem_codomain = fem_codomain - else: - self._fem_codomain = fem_domain - self._domain = self._fem_domain.coeff_space - self._codomain = self._fem_codomain.coeff_space - - self._matrix = matrix - self._sparse_matrix = sparse_matrix - - @property - def domain( self ): - return self._domain - - @property - def codomain( self ): - return self._codomain - - @property - def fem_domain( self ): - return self._fem_domain - - @property - def fem_codomain( self ): - return self._fem_codomain - - @property - def matrix( self ): - return self._matrix - - @property - def T(self): - return self.transpose() - - @property - def dtype( self ): - return self.domain.dtype - - def toarray(self): - return self._matrix.toarray() - #raise NotImplementedError('toarray() is not defined for FEMLinearOperators.') - - def tosparse(self): - return self._matrix.tosparse() - #raise NotImplementedError('tosparse() is not defined for FEMLinearOperators.') - - # ... - def transpose(self, conjugate=False): - raise NotImplementedError('Class does not provide a transpose() method') - - # ... - def to_sparse_matrix( self , **kwargs): - if self._sparse_matrix is not None: - return self._sparse_matrix - elif self._matrix is not None: - return self._matrix.tosparse() - else: - raise NotImplementedError('Class does not provide a get_sparse_matrix() method without a matrix') - - # ... - def __call__( self, f ): - if self._matrix is not None: - coeffs = self._matrix.dot(f.coeffs) - return FemField(self.fem_codomain, coeffs=coeffs) - else: - raise NotImplementedError('Class does not provide a __call__ method without a matrix') - - # ... - def dot( self, f_coeffs, out=None ): - # coeffs layer - if self._matrix is not None: - f = FemField(self.fem_domain, coeffs=f_coeffs) - return self(f).coeffs - else: - raise NotImplementedError('Class does not provide a dot method without a matrix') - - # ... - def __mul__(self, c): - return MultLinearOperator(c, self) - - # ... - def __add__(self, C): - assert isinstance(C, FemLinearOperator) - return SumLinearOperator(C, self) - - # ... - def __sub__(self, C): - assert isinstance(C, FemLinearOperator) - return SumLinearOperator(C, -self) - - # ... - def __neg__(self): - return MultLinearOperator(-1, self) - - -#============================================================================== -class ComposedLinearOperator( FemLinearOperator ): - """ - operator L = L_1 .. L_n - with L_i = self._operators[i-1] - (so, the last one is applied first, like in a product) - """ - def __init__( self, operators ): - n = len(operators) - assert all([isinstance(operators[i], FemLinearOperator) for i in range(n)]) - assert all([operators[i].fem_domain == operators[i+1].fem_codomain for i in range(n-1)]) - FemLinearOperator.__init__( - self, fem_domain=operators[-1].fem_domain, fem_codomain=operators[0].fem_codomain - ) - self._operators = operators - self._n = n - - # matrix not defined by matrix product because it could break the Stencil Matrix structure - - def to_sparse_matrix( self, **kwargs): - mat = self._operators[-1].to_sparse_matrix() - for i in range(2, self._n+1): - mat = self._operators[-i].to_sparse_matrix() * mat - return mat - - def __call__( self, f ): - v = self._operators[-1](f) - for i in range(2, self._n+1): - v = self._operators[-i](v) - return v - - def dot( self, f_coeffs, out=None ): - v_coeffs = self._operators[-1].dot(f_coeffs) - for i in range(2, self._n+1): - v_coeffs = self._operators[-i].dot(v_coeffs) - return v_coeffs - - -#============================================================================== -class IdLinearOperator( FemLinearOperator ): - - def __init__( self, V ): - FemLinearOperator.__init__(self, fem_domain=V) - - def to_sparse_matrix( self , **kwargs): - return sparse_id( self.fem_domain.nbasis ) - - def __call__( self, f ): - return f - - def dot( self, f_coeffs, out=None ): - return f_coeffs - -#============================================================================== -class SumLinearOperator( FemLinearOperator ): - - def __init__( self, B, A ): - assert isinstance(A, FemLinearOperator) - assert isinstance(B, FemLinearOperator) - assert B.fem_domain == A.fem_domain - assert B.fem_codomain == A.fem_codomain - FemLinearOperator.__init__( - self, fem_domain=A.fem_domain, fem_codomain=A.fem_codomain - ) - self._A = A - self._B = B - - def to_sparse_matrix( self, **kwargs): - return self._A.to_sparse_matrix() + self._B.to_sparse_matrix() - - def __call__( self, f ): - # fem layer - return self._B(f) + self._A(f) - - def dot( self, f_coeffs, out=None ): - # coeffs layer - return self._B.dot(f_coeffs) + self._A.dot(f_coeffs) - -#============================================================================== -class MultLinearOperator( FemLinearOperator ): - - def __init__( self, c, A ): - assert isinstance(A, FemLinearOperator) - FemLinearOperator.__init__( - self, fem_domain=A.fem_domain, fem_codomain=A.fem_codomain - ) - self._A = A - self._c = c - - def to_sparse_matrix( self, **kwargs): - return self._c * self._A.to_sparse_matrix() - - def __call__( self, f ): - # fem layer - return self._c * self._A(f) - - def dot( self, f_coeffs, out=None ): - # coeffs layer - return self._c * self._A.dot(f_coeffs) - diff --git a/psydac/feec/multipatch/operators.py b/psydac/feec/multipatch/operators.py deleted file mode 100644 index 0ee00210c..000000000 --- a/psydac/feec/multipatch/operators.py +++ /dev/null @@ -1,1248 +0,0 @@ -# coding: utf-8 - -# Conga operators on piecewise (broken) de Rham sequences - -from sympy import Tuple -from mpi4py import MPI -import os -import numpy as np - -from scipy.sparse import save_npz, load_npz -from scipy.sparse import kron, block_diag -from scipy.sparse.linalg import inv - -from sympde.topology import Boundary, Interface, Union -from sympde.topology import element_of, elements_of -from sympde.topology.space import ScalarFunction -from sympde.calculus import grad, dot, inner, rot, div -from sympde.calculus import laplace, bracket, convect -from sympde.calculus import jump, avg, Dn, minus, plus -from sympde.expr.expr import LinearForm, BilinearForm -from sympde.expr.expr import integral - -from psydac.core.bsplines import collocation_matrix, histopolation_matrix - -from psydac.api.discretization import discretize -from psydac.api.essential_bc import apply_essential_bc_stencil -from psydac.api.settings import PSYDAC_BACKENDS -from psydac.linalg.block import BlockVectorSpace, BlockVector, BlockLinearOperator -from psydac.linalg.stencil import StencilVector, StencilMatrix, StencilInterfaceMatrix -from psydac.linalg.solvers import inverse -from psydac.fem.basic import FemField - - -from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_L2 -from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D -from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator - - -def get_patch_index_from_face(domain, face): - """ Return the patch index of subdomain/boundary - - Parameters - ---------- - domain : - The Symbolic domain - - face : - A patch or a boundary of a patch - - Returns - ------- - i : - The index of a subdomain/boundary in the multipatch domain - """ - - if domain.mapping: - domain = domain.logical_domain - if face.mapping: - face = face.logical_domain - - domains = domain.interior.args - if isinstance(face, Interface): - raise NotImplementedError( - "This face is an interface, it has several indices -- I am a machine, I cannot choose. Help.") - elif isinstance(face, Boundary): - i = domains.index(face.domain) - else: - i = domains.index(face) - return i - - -def get_interface_from_corners(corner1, corner2, domain): - """ Return the interface between two corners from two different patches that correspond to a single (physical) vertex. - - Parameters - ---------- - corner1 : - The first corner of the 2D interface - - corner2 : - The second corner of the 2D interface - - domain : - The Symbolic domain - - Returns - ------- - interface: - The interface between two vertices - - """ - - interface = [] - interfaces = domain.interfaces - - if not isinstance(interfaces, Union): - interfaces = (interfaces,) - - for i in interfaces: - if i.plus.domain in [corner1.domain, corner2.domain]: - if i.minus.domain in [corner1.domain, corner2.domain]: - interface.append(i) - - bd1 = corner1.boundaries - bd2 = corner2.boundaries - - new_interface = [] - - for i in interface: - if i.minus in bd1 + bd2: - if i.plus in bd2 + bd1: - new_interface.append(i) - - if len(new_interface) == 1: - return new_interface[0] - if len(new_interface) > 1: - raise ValueError( - 'found more than one interface for the corners {} and {}'.format( - corner1, corner2)) - return None - - -def get_row_col_index(corner1, corner2, interface, axis, V1, V2): - """ Return the row and column index of a corner in the StencilInterfaceMatrix - for dofs of H1 type spaces - - Parameters - ---------- - corner1 : - The first corner of the 2D interface - - corner2 : - The second corner of the 2D interface - - interface : - The interface between the two corners - - axis : - Axis of the interface - - V1 : - Test Space - - V2 : - Trial Space - - Returns - ------- - index: - The StencilInterfaceMatrix index of the corner, it has the form (i1, i2, k1, k2) in 2D, - where (i1, i2) identifies the row and (k1, k2) the diagonal. - """ - start = V1.coeff_space.starts - end = V1.coeff_space.ends - degree = V2.degree - start_end = (start, end) - - row = [None] * len(start) - col = [0] * len(start) - - assert corner1.boundaries[0].axis == corner2.boundaries[0].axis - - for bd in corner1.boundaries: - row[bd.axis] = start_end[(bd.ext + 1) // 2][bd.axis] - - if interface is None and corner1.domain != corner2.domain: - bd = [i for i in corner1.boundaries if i.axis == axis][0] - if bd.ext == 1: - row[bd.axis] = degree[bd.axis] - - if interface is None: - return row + col - - axis = interface.axis - - if interface.minus.domain == corner1.domain: - if interface.minus.ext == -1: - row[axis] = 0 - else: - row[axis] = degree[axis] - else: - if interface.plus.ext == -1: - row[axis] = 0 - else: - row[axis] = degree[axis] - - if interface.minus.ext == interface.plus.ext: - pass - elif interface.minus.domain == corner1.domain: - if interface.minus.ext == -1: - col[axis] = degree[axis] - else: - col[axis] = -degree[axis] - else: - if interface.plus.ext == -1: - col[axis] = degree[axis] - else: - col[axis] = -degree[axis] - - return row + col - - -# =============================================================================== -def allocate_interface_matrix(corners, test_space, trial_space): - """ Allocate the interface matrix for a vertex shared by two patches - - Parameters - ---------- - corners: - The patch corners corresponding to the common shared vertex - - test_space: - The test space - - trial_space: - The trial space - - Returns - ------- - mat: - The interface matrix shared by two patches - """ - bi, bj = list(zip(*corners)) - permutation = np.arange(bi[0].domain.dim) - - flips = [] - k = 0 - while k < len(bi): - c1 = np.array(bi[k].coordinates) - c2 = np.array(bj[k].coordinates)[permutation] - flips.append( - np.array([-1 if d1 != d2 else 1 for d1, d2 in zip(c1, c2)])) - - if np.sum(abs(flips[0] - flips[-1])) != 0: - prod = [f1 * f2 for f1, f2 in zip(flips[0], flips[-1])] - while -1 in prod: - i1 = prod.index(-1) - if -1 in prod[i1 + 1:]: - i2 = i1 + 1 + prod[i1 + 1:].index(-1) - prod = prod[i2 + 1:] - permutation[i1], permutation[i2] = permutation[i2], permutation[i1] - k = -1 - flips = [] - else: - break - - k += 1 - - assert all(abs(flips[0] - i).sum() == 0 for i in flips) - cs = list(zip(*[i.coordinates for i in bi])) - axis = [all(i[0] == j for j in i) for i in cs].index(True) - ext = 1 if cs[axis][0] == 1 else -1 - s = test_space.get_assembly_grids( - )[axis].spans[-1 if ext == 1 else 0] - test_space.degree[axis] - - mat = StencilInterfaceMatrix( - trial_space.coeff_space, - test_space.coeff_space, - s, - s, - axis, - flip=flips[0], - permutation=list(permutation)) - return mat - -# =============================================================================== -# The following operators are not compatible with the changes in the Stencil format -# and their datatype does not allow for non-matching interfaces, but they might be -# useful for future implementations -# =============================================================================== - - -class ConformingProjection_V0(FemLinearOperator): - """ - Conforming projection from global broken V0 space to conforming global V0 space - Defined by averaging of interface dofs - - Parameters - ---------- - V0h: - The discrete space - - domain_h: - The discrete domain of the projector - - hom_bc : - Apply homogenous boundary conditions if True - - backend_language: - The backend used to accelerate the code - - storage_fn: - filename to store/load the operator sparse matrix - """ - # todo (MCP, 16.03.2021): - # - avoid discretizing a bilinear form - # - allow case without interfaces (single or multipatch) - - def __init__( - self, - V0h, - domain_h, - hom_bc=False, - backend_language='python', - storage_fn=None): - - FemLinearOperator.__init__(self, fem_domain=V0h) - - V0 = V0h.symbolic_space - domain = V0.domain - self.symbolic_domain = domain - - if storage_fn and os.path.exists(storage_fn): - print( - "[ConformingProjection_V0] loading operator sparse matrix from " + - storage_fn) - self._sparse_matrix = load_npz(storage_fn) - - else: - # assemble the operator matrix - u, v = elements_of(V0, names='u, v') - expr = u * v # dot(u,v) - - Interfaces = domain.interfaces # note: interfaces does not include the boundary - # this penalization is for an H1-conforming space - expr_I = (plus(u) - minus(u)) * (plus(v) - minus(v)) - - a = BilinearForm((u, v), integral(domain, expr) + - integral(Interfaces, expr_I)) - # print('[[ forcing python backend for ConformingProjection_V0]] ') - # backend_language = 'python' - ah = discretize( - a, domain_h, [ - V0h, V0h], backend=PSYDAC_BACKENDS[backend_language]) - - # self._A = ah.assemble() - self._A = ah.forms[0]._matrix - - spaces = self._A.domain.spaces - - if isinstance(Interfaces, Interface): - Interfaces = (Interfaces, ) - - for b1 in self._A.blocks: - for A in b1: - if A is None: - continue - A[:, :, :, :] = 0 - - indices = [slice(None, None)] * domain.dim + [0] * domain.dim - - for i in range(len(self._A.blocks)): - self._A[i, i][tuple(indices)] = 1 - - for I in Interfaces: - - axis = I.axis - i_minus = get_patch_index_from_face(domain, I.minus) - i_plus = get_patch_index_from_face(domain, I.plus) - - sp_minus = spaces[i_minus] - sp_plus = spaces[i_plus] - - s_minus = sp_minus.starts[axis] - e_minus = sp_minus.ends[axis] - - s_plus = sp_plus.starts[axis] - e_plus = sp_plus.ends[axis] - - d_minus = V0h.spaces[i_minus].degree[axis] - d_plus = V0h.spaces[i_plus].degree[axis] - - indices = [slice(None, None)] * domain.dim + [0] * domain.dim - - minus_ext = I.minus.ext - plus_ext = I.plus.ext - - if minus_ext == 1: - indices[axis] = e_minus - else: - indices[axis] = s_minus - self._A[i_minus, i_minus][tuple(indices)] = 1 / 2 - - if plus_ext == 1: - indices[axis] = e_plus - else: - indices[axis] = s_plus - - self._A[i_plus, i_plus][tuple(indices)] = 1 / 2 - - if plus_ext == minus_ext: - if minus_ext == 1: - indices[axis] = d_minus - else: - indices[axis] = s_minus - - self._A[i_minus, i_plus][tuple(indices)] = 1 / 2 - - if plus_ext == 1: - indices[axis] = d_plus - else: - indices[axis] = s_plus - - self._A[i_plus, i_minus][tuple(indices)] = 1 / 2 - - else: - if minus_ext == 1: - indices[axis] = d_minus - else: - indices[axis] = s_minus - - if plus_ext == 1: - indices[domain.dim + axis] = d_plus - else: - indices[domain.dim + axis] = -d_plus - - self._A[i_minus, i_plus][tuple(indices)] = 1 / 2 - - if plus_ext == 1: - indices[axis] = d_plus - else: - indices[axis] = s_plus - - if minus_ext == 1: - indices[domain.dim + axis] = d_minus - else: - indices[domain.dim + axis] = -d_minus - - self._A[i_plus, i_minus][tuple(indices)] = 1 / 2 - - domain = domain.logical_domain - corner_blocks = {} - for c in domain.corners: - for b1 in c.corners: - i = get_patch_index_from_face(domain, b1.domain) - for b2 in c.corners: - j = get_patch_index_from_face(domain, b2.domain) - if (i, j) in corner_blocks: - corner_blocks[i, j] += [(b1, b2)] - else: - corner_blocks[i, j] = [(b1, b2)] - - for c in domain.corners: - if len(c) == 2: - continue - for b1 in c.corners: - i = get_patch_index_from_face(domain, b1.domain) - for b2 in c.corners: - j = get_patch_index_from_face(domain, b2.domain) - interface = get_interface_from_corners(b1, b2, domain) - axis = None - if self._A[i, j] is None: - self._A[i, j] = allocate_interface_matrix( - corner_blocks[i, j], V0h.spaces[i], V0h.spaces[j]) - - if i != j and self._A[i, j]: - axis = self._A[i, j]._dim - index = get_row_col_index( - b1, b2, interface, axis, V0h.spaces[i], V0h.spaces[j]) - self._A[i, j][tuple(index)] = 1 / len(c) - - if hom_bc: - for bn in domain.boundary: - self.set_homogenous_bc(bn) - - self._matrix = self._A - self._sparse_matrix = self._matrix.tosparse() # self._sparse_matrix - - if storage_fn: - print( - "[ConformingProjection_V0] storing operator sparse matrix in " + - storage_fn) - save_npz(storage_fn, self._sparse_matrix) - - def set_homogenous_bc(self, boundary, rhs=None): - domain = self.symbolic_domain - Vh = self.fem_domain - if domain.mapping: - domain = domain.logical_domain - if boundary.mapping: - boundary = boundary.logical_domain - - corners = domain.corners - i = get_patch_index_from_face(domain, boundary) - if rhs: - apply_essential_bc_stencil( - rhs[i], axis=boundary.axis, ext=boundary.ext, order=0) - for j in range(len(domain)): - if self._A[i, j] is None: - continue - apply_essential_bc_stencil( - self._A[i, j], axis=boundary.axis, ext=boundary.ext, order=0) - - for c in corners: - faces = [f for b in c.corners for f in b.boundaries] - if len(c) == 2: - continue - if boundary in faces: - for b1 in c.corners: - i = get_patch_index_from_face(domain, b1.domain) - for b2 in c.corners: - j = get_patch_index_from_face(domain, b2.domain) - interface = get_interface_from_corners(b1, b2, domain) - axis = None - if i != j: - axis = self._A[i, j].dim - index = get_row_col_index( - b1, b2, interface, axis, Vh.spaces[i], Vh.spaces[j]) - self._A[i, j][tuple(index)] = 0. - - if i == j and rhs: - rhs[i][tuple(index[:2])] = 0. - -# =============================================================================== - - -class ConformingProjection_V1(FemLinearOperator): - """ - Conforming projection from global broken V1 space to conforming V1 global space - - proj.dot(v) returns the conforming projection of v, computed by solving linear system - - Parameters - ---------- - V1h: - The discrete space - - domain_h: - The discrete domain of the projector - - hom_bc : - Apply homogenous boundary conditions if True - - backend_language: - The backend used to accelerate the code - - storage_fn: - filename to store/load the operator sparse matrix - """ - # todo (MCP, 16.03.2021): - # - avoid discretizing a bilinear form - # - allow case without interfaces (single or multipatch) - - def __init__( - self, - V1h, - domain_h, - hom_bc=False, - backend_language='python', - storage_fn=None): - - FemLinearOperator.__init__(self, fem_domain=V1h) - - V1 = V1h.symbolic_space - domain = V1.domain - self.symbolic_domain = domain - - if storage_fn and os.path.exists(storage_fn): - print( - "[ConformingProjection_V1] loading operator sparse matrix from " + - storage_fn) - self._sparse_matrix = load_npz(storage_fn) - - else: - # assemble the operator matrix - u, v = elements_of(V1, names='u, v') - expr = dot(u, v) - # - Interfaces = domain.interfaces # note: interfaces does not include the boundary - # this penalization is for an H1-conforming space - expr_I = dot(plus(u) - minus(u), plus(v) - minus(v)) - - a = BilinearForm((u, v), integral(domain, expr) + - integral(Interfaces, expr_I)) - # print('[[ forcing python backend for ConformingProjection_V1]] ') - # backend_language = 'python' - ah = discretize( - a, domain_h, [ - V1h, V1h], backend=PSYDAC_BACKENDS[backend_language]) - # - # # self._A = ah.assemble() - self._A = ah.forms[0]._matrix - # C1 = V1h.coeff_space - # self._A = BlockLinearOperator(C1, C1) - - for b1 in self._A.blocks: - for b2 in b1: - if b2 is None: - continue - for b3 in b2.blocks: - for A in b3: - if A is None: - continue - A[:, :, :, :] = 0 - - spaces = self._A.domain.spaces - - if isinstance(Interfaces, Interface): - Interfaces = (Interfaces, ) - - indices = [slice(None, None)] * domain.dim + [0] * domain.dim - - for i in range(len(self._A.blocks)): - self._A[i, i][0, 0][tuple(indices)] = 1 - self._A[i, i][1, 1][tuple(indices)] = 1 - - # empty list if no interfaces ? - if Interfaces is not None: - - for I in Interfaces: - - i_minus = get_patch_index_from_face(domain, I.minus) - i_plus = get_patch_index_from_face(domain, I.plus) - - indices = [slice(None, None)] * \ - domain.dim + [0] * domain.dim - - sp1 = spaces[i_minus] - sp2 = spaces[i_plus] - - s11 = sp1.spaces[0].starts[I.axis] - e11 = sp1.spaces[0].ends[I.axis] - s12 = sp1.spaces[1].starts[I.axis] - e12 = sp1.spaces[1].ends[I.axis] - - s21 = sp2.spaces[0].starts[I.axis] - e21 = sp2.spaces[0].ends[I.axis] - s22 = sp2.spaces[1].starts[I.axis] - e22 = sp2.spaces[1].ends[I.axis] - - d11 = V1h.spaces[i_minus].spaces[0].degree[I.axis] - d12 = V1h.spaces[i_minus].spaces[1].degree[I.axis] - - d21 = V1h.spaces[i_plus].spaces[0].degree[I.axis] - d22 = V1h.spaces[i_plus].spaces[1].degree[I.axis] - - s_minus = [s11, s12] - e_minus = [e11, e12] - - s_plus = [s21, s22] - e_plus = [e21, e22] - - d_minus = [d11, d12] - d_plus = [d21, d22] - - minus_ext = I.minus.ext - plus_ext = I.plus.ext - - axis = I.axis - for k in range(domain.dim): - if k == I.axis: - continue - - if minus_ext == 1: - indices[axis] = e_minus[k] - else: - indices[axis] = s_minus[k] - self._A[i_minus, i_minus][k, k][tuple(indices)] = 1 / 2 - - if plus_ext == 1: - indices[axis] = e_plus[k] - else: - indices[axis] = s_plus[k] - - self._A[i_plus, i_plus][k, k][tuple(indices)] = 1 / 2 - - if plus_ext == minus_ext: - if minus_ext == 1: - indices[axis] = d_minus[k] - else: - indices[axis] = s_minus[k] - - self._A[i_minus, i_plus][k, k][tuple( - indices)] = 1 / 2 * I.direction - - if plus_ext == 1: - indices[axis] = d_plus[k] - else: - indices[axis] = s_plus[k] - - self._A[i_plus, i_minus][k, k][tuple( - indices)] = 1 / 2 * I.direction - - else: - if minus_ext == 1: - indices[axis] = d_minus[k] - else: - indices[axis] = s_minus[k] - - if plus_ext == 1: - indices[domain.dim + axis] = d_plus[k] - else: - indices[domain.dim + axis] = -d_plus[k] - - self._A[i_minus, i_plus][k, k][tuple( - indices)] = 1 / 2 * I.direction - - if plus_ext == 1: - indices[axis] = d_plus[k] - else: - indices[axis] = s_plus[k] - - if minus_ext == 1: - indices[domain.dim + axis] = d_minus[k] - else: - indices[domain.dim + axis] = -d_minus[k] - - self._A[i_plus, i_minus][k, k][tuple( - indices)] = 1 / 2 * I.direction - - if hom_bc: - for bn in domain.boundary: - self.set_homogenous_bc(bn) - - self._matrix = self._A - self._sparse_matrix = self._matrix.tosparse() - - if storage_fn: - print( - "[ConformingProjection_V1] storing operator sparse matrix in " + - storage_fn) - save_npz(storage_fn, self._sparse_matrix) - - def set_homogenous_bc(self, boundary): - domain = self.symbolic_domain - Vh = self.fem_domain - - i = get_patch_index_from_face(domain, boundary) - axis = boundary.axis - ext = boundary.ext - for j in range(len(domain)): - if self._A[i, j] is None: - continue - apply_essential_bc_stencil( - self._A[i, j][1 - axis, 1 - axis], axis=axis, ext=ext, order=0) - - -# =============================================================================== -def get_K0_and_K0_inv(V0h, uniform_patches=False): - """ - Compute the change of basis matrices K0 and K0^{-1} in V0h. - - With - K0_ij = sigma^0_i(B_j) = B_jx(n_ix) * B_jy(n_iy) - where sigma_i is the geometric (interpolation) dof - and B_j is the tensor-product B-spline - """ - if uniform_patches: - print(' [[WARNING -- hack in get_K0_and_K0_inv: using copies of 1st-patch matrices in every patch ]] ') - - V0 = V0h.symbolic_space # VOh is FemSpace - domain = V0.domain - K0_blocks = [] - K0_inv_blocks = [] - for k, D in enumerate(domain.interior): - if uniform_patches and k > 0: - K0_k = K0_blocks[0].copy() - K0_inv_k = K0_inv_blocks[0].copy() - - else: - V0_k = V0h.spaces[k] # fem space on patch k: (TensorFemSpace) - K0_k_factors = [None, None] - for d in [0, 1]: - # 1d fem space alond dim d (SplineSpace) - V0_kd = V0_k.spaces[d] - K0_k_factors[d] = collocation_matrix( - knots=V0_kd.knots, - degree=V0_kd.degree, - periodic=V0_kd.periodic, - normalization=V0_kd.basis, - xgrid=V0_kd.greville - ) - K0_k = kron(*K0_k_factors) - K0_k.eliminate_zeros() - K0_inv_k = inv(K0_k.tocsc()) - K0_inv_k.eliminate_zeros() - - K0_blocks.append(K0_k) - K0_inv_blocks.append(K0_inv_k) - K0 = block_diag(K0_blocks) - K0_inv = block_diag(K0_inv_blocks) - return K0, K0_inv - - -# =============================================================================== -def get_K1_and_K1_inv(V1h, uniform_patches=False): - """ - Compute the change of basis matrices K1 and K1^{-1} in Hcurl space V1h. - - With - K1_ij = sigma^1_i(B_j) = int_{e_ix}(M_jx) * B_jy(n_iy) - if i = horizontal edge [e_ix, n_iy] and j = (M_jx o B_jy) x-oriented MoB spline - or - = B_jx(n_ix) * int_{e_iy}(M_jy) - if i = vertical edge [n_ix, e_iy] and j = (B_jx o M_jy) y-oriented BoM spline - (above, 'o' denotes tensor-product for functions) - """ - if uniform_patches: - print(' [[WARNING -- hack in get_K1_and_K1_inv: using copies of 1st-patch matrices in every patch ]] ') - - V1 = V1h.symbolic_space # V1h is FemSpace - domain = V1.domain - K1_blocks = [] - K1_inv_blocks = [] - for k, D in enumerate(domain.interior): - if uniform_patches and k > 0: - K1_k = K1_blocks[0].copy() - K1_inv_k = K1_inv_blocks[0].copy() - - else: - # fem space on patch k: - V1_k = V1h.spaces[k] - K1_k_blocks = [] - for c in [0, 1]: # dim of component - # fem space for comp. dc (TensorFemSpace) - V1_kc = V1_k.spaces[c] - K1_kc_factors = [None, None] - for d in [0, 1]: # dim of variable - # 1d fem space for comp c alond dim d (SplineSpace) - V1_kcd = V1_kc.spaces[d] - if c == d: - K1_kc_factors[d] = histopolation_matrix( - knots=V1_kcd.knots, - degree=V1_kcd.degree, - periodic=V1_kcd.periodic, - normalization=V1_kcd.basis, - xgrid=V1_kcd.ext_greville - ) - else: - K1_kc_factors[d] = collocation_matrix( - knots=V1_kcd.knots, - degree=V1_kcd.degree, - periodic=V1_kcd.periodic, - normalization=V1_kcd.basis, - xgrid=V1_kcd.greville - ) - K1_kc = kron(*K1_kc_factors) - K1_kc.eliminate_zeros() - K1_k_blocks.append(K1_kc) - K1_k = block_diag(K1_k_blocks) - K1_k.eliminate_zeros() - K1_inv_k = inv(K1_k.tocsc()) - K1_inv_k.eliminate_zeros() - - K1_blocks.append(K1_k) - K1_inv_blocks.append(K1_inv_k) - - K1 = block_diag(K1_blocks) - K1_inv = block_diag(K1_inv_blocks) - return K1, K1_inv - - -# #=============================================================================== -# def get_M_and_M_inv(Vh, subdomains_h, is_scalar, backend_language='python'): -# """ -# compute the mass matrix M and M^{-1} in multipatch space Vh -# DOES NOT WORK -- SHOULD WE HAVE THE POSSIBILITY OF DOING THAT ? -# """ -# from pprint import pprint -# -# V = Vh.symbolic_space # VOh is FemSpace -# domain = V.domain -# M_blocks = [] -# M_inv_blocks = [] -# -# # print('type(domain_h) = ', type(domain_h)) -# # -# # print('type(domain_h._patches) = ', type(domain_h._patches)) -# # print('len(domain_h._patches) = ', len(domain_h._patches)) -# # -# # mappings = domain_h.mappings -# # print('type(mappings) = ', type(mappings)) -# # print('len(mappings) = ', len(mappings)) -# # -# # mappings_list = list(mappings.values()) -# # print('len(mappings_list) = ', len(mappings_list)) -# # -# # print('type(mappings_list[0]) = ', type(mappings_list[0])) -# -# for k, Dh_k in enumerate(subdomains_h): -# -# print('k = ', k) -# print('type(Dh_k) = ', type(Dh_k)) -# # print('Dh = ', Dh) -# D_k = domain.interior[k] -# -# # exit() -# -# # for k, D in enumerate(domain.interior): -# -# V_k = V.spaces[k] -# Vh_k = Vh.spaces[k] -# -# # print(type(domain_h)) -# # -# # pprint(dir(domain_h)) -# # -# # -# # print(len(domain_h._patches)) -# # exit() -# # Dh_k = domain_h.spaces[k] # fem space on patch k: (TensorFemSpace) -# u, v = elements_of(V_k, names='u, v') -# if is_scalar: -# expr = u*v -# else: -# expr = dot(u,v) -# a_k = BilinearForm((u,v), integral(D_k, expr)) -# a_kh = discretize(a_k, Dh_k, [Vh_k, Vh_k], backend=PSYDAC_BACKENDS[backend_language]) # 'pyccel-gcc']) -# -# M_k = a_kh.assemble().toarray() -# M_k.eliminate_zeros() -# M_inv_k = inv(M_k.tocsc()) -# M_inv_k.eliminate_zeros() -# -# M_blocks.append(M_k) -# M_inv_blocks.append(M_inv_k) -# M = block_diag(M_blocks) -# M_inv = block_diag(M_inv_blocks) -# return M, M_inv - -# =============================================================================== -class HodgeOperator(FemLinearOperator): - """ - Change of basis operator: dual basis -> primal basis - - self._matrix: matrix of the primal Hodge = this is the mass matrix ! - self.dual_Hodge_matrix: this is the INVERSE mass matrix - - Parameters - ---------- - Vh: - The discrete space - - domain_h: - The discrete domain of the projector - - metric : - the metric of the de Rham complex - - backend_language: - The backend used to accelerate the code - - load_dir: - storage files for the primal and dual Hodge sparse matrice - - load_space_index: - the space index in the derham sequence - - Notes - ----- - Either we use a storage, or these matrices are only computed on demand - # todo: we compute the sparse matrix when to_sparse_matrix is called -- but never the stencil matrix (should be fixed...) - We only support the identity metric, this implies that the dual Hodge is the inverse of the primal one. - # todo: allow for non-identity metrics - """ - - def __init__( - self, - Vh, - domain_h, - metric='identity', - backend_language='python', - load_dir=None, - load_space_index=''): - - FemLinearOperator.__init__(self, fem_domain=Vh) - self._domain_h = domain_h - self._backend_language = backend_language - self._dual_Hodge_sparse_matrix = None - - assert metric == 'identity' - self._metric = metric - - if load_dir and isinstance(load_dir, str): - if not os.path.exists(load_dir): - os.makedirs(load_dir) - assert str(load_space_index) in ['0', '1', '2', '3'] - primal_Hodge_storage_fn = load_dir + \ - '/H{}_m.npz'.format(load_space_index) - dual_Hodge_storage_fn = load_dir + \ - '/dH{}_m.npz'.format(load_space_index) - - primal_Hodge_is_stored = os.path.exists(primal_Hodge_storage_fn) - dual_Hodge_is_stored = os.path.exists(dual_Hodge_storage_fn) - if dual_Hodge_is_stored: - assert primal_Hodge_is_stored - print( - " ... loading dual Hodge sparse matrix from " + - dual_Hodge_storage_fn) - self._dual_Hodge_sparse_matrix = load_npz( - dual_Hodge_storage_fn) - print( - "[HodgeOperator] loading primal Hodge sparse matrix from " + - primal_Hodge_storage_fn) - self._sparse_matrix = load_npz(primal_Hodge_storage_fn) - else: - assert not primal_Hodge_is_stored - print( - "[HodgeOperator] assembling both sparse matrices for storage...") - self.assemble_primal_Hodge_matrix() - print( - "[HodgeOperator] storing primal Hodge sparse matrix in " + - primal_Hodge_storage_fn) - save_npz(primal_Hodge_storage_fn, self._sparse_matrix) - self.assemble_dual_Hodge_matrix() - print( - "[HodgeOperator] storing dual Hodge sparse matrix in " + - dual_Hodge_storage_fn) - save_npz(dual_Hodge_storage_fn, self._dual_Hodge_sparse_matrix) - else: - # matrices are not stored, we will probably compute them later - pass - - def to_sparse_matrix(self): - """ - the Hodge matrix is the patch-wise multi-patch mass matrix - it is not stored by default but assembled on demand - """ - - if (self._sparse_matrix is not None) or (self._matrix is not None): - return FemLinearOperator.to_sparse_matrix(self) - - self.assemble_primal_Hodge_matrix() - - return self._sparse_matrix - - def assemble_primal_Hodge_matrix(self): - """ - the Hodge matrix is the patch-wise multi-patch mass matrix - it is not stored by default but assembled on demand - """ - - if self._matrix is None: - Vh = self.fem_domain - assert Vh == self.fem_codomain - - V = Vh.symbolic_space - domain = V.domain - # domain_h = V0h.domain: would be nice... - u, v = elements_of(V, names='u, v') - - if isinstance(u, ScalarFunction): - expr = u * v - else: - expr = dot(u, v) - - a = BilinearForm((u, v), integral(domain, expr)) - ah = discretize(a, self._domain_h, [ - Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language]) - - self._matrix = ah.assemble() # Mass matrix in stencil format - self._sparse_matrix = self._matrix.tosparse() - - def get_dual_Hodge_sparse_matrix(self): - if self._dual_Hodge_sparse_matrix is None: - self.assemble_dual_Hodge_matrix() - - return self._dual_Hodge_sparse_matrix - - def assemble_dual_Hodge_matrix(self): - """ - the dual Hodge matrix is the patch-wise inverse of the multi-patch mass matrix - it is not stored by default but computed on demand, by local (patch-wise) inversion of the mass matrix - """ - - if self._dual_Hodge_sparse_matrix is None: - if not self._matrix: - self.assemble_primal_Hodge_matrix() - - M = self._matrix # mass matrix of the (primal) basis - nrows = M.n_block_rows - ncols = M.n_block_cols - - inv_M_blocks = [] - for i in range(nrows): - Mii = M[i, i].tosparse() - inv_Mii = inv(Mii.tocsc()) - inv_Mii.eliminate_zeros() - inv_M_blocks.append(inv_Mii) - - inv_M = block_diag(inv_M_blocks) - self._dual_Hodge_sparse_matrix = inv_M - -# ============================================================================== - - -class BrokenGradient_2D(FemLinearOperator): - - def __init__(self, V0h, V1h): - - FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V1h) - - D0s = [Gradient_2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] - - self._matrix = BlockLinearOperator(self.domain, self.codomain, blocks={ - (i, i): D0i._matrix for i, D0i in enumerate(D0s)}) - - def transpose(self, conjugate=False): - # todo (MCP): define as the dual differential operator - return BrokenTransposedGradient_2D(self.fem_domain, self.fem_codomain) - -# ============================================================================== - - -class BrokenTransposedGradient_2D(FemLinearOperator): - - def __init__(self, V0h, V1h): - - FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V0h) - - D0s = [Gradient_2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] - - self._matrix = BlockLinearOperator(self.domain, self.codomain, blocks={ - (i, i): D0i._matrix.T for i, D0i in enumerate(D0s)}) - - def transpose(self, conjugate=False): - # todo (MCP): discard - return BrokenGradient_2D(self.fem_codomain, self.fem_domain) - - -# ============================================================================== -class BrokenScalarCurl_2D(FemLinearOperator): - def __init__(self, V1h, V2h): - - FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V2h) - - D1s = [ScalarCurl_2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] - - self._matrix = BlockLinearOperator(self.domain, self.codomain, blocks={ - (i, i): D1i._matrix for i, D1i in enumerate(D1s)}) - - def transpose(self, conjugate=False): - return BrokenTransposedScalarCurl_2D( - V1h=self.fem_domain, V2h=self.fem_codomain) - - -# ============================================================================== -class BrokenTransposedScalarCurl_2D(FemLinearOperator): - - def __init__(self, V1h, V2h): - - FemLinearOperator.__init__(self, fem_domain=V2h, fem_codomain=V1h) - - D1s = [ScalarCurl_2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] - - self._matrix = BlockLinearOperator(self.domain, self.codomain, blocks={ - (i, i): D1i._matrix.T for i, D1i in enumerate(D1s)}) - - def transpose(self, conjugate=False): - return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) - - -# ============================================================================== - -# def multipatch_Moments_Hcurl(f, V1h, domain_h): - -def ortho_proj_Hcurl(EE, V1h, domain_h, M1, backend_language='python'): - """ - return orthogonal projection of E on V1h, given M1 the mass matrix - """ - assert isinstance(EE, Tuple) - V1 = V1h.symbolic_space - v = element_of(V1, name='v') - l = LinearForm(v, integral(V1.domain, dot(v, EE))) - lh = discretize( - l, - domain_h, - V1h, - backend=PSYDAC_BACKENDS[backend_language]) - b = lh.assemble() - M1_inv = inverse(M1.mat(), 'pcg', pc='jacobi', tol=1e-10) - sol_coeffs = M1_inv @ b - - return FemField(V1h, coeffs=sol_coeffs) - -# ============================================================================== - - -class Multipatch_Projector_H1: - """ - to apply the H1 projection (2D) on every patch - """ - - def __init__(self, V0h): - - self._P0s = [Projector_H1(V) for V in V0h.spaces] - self._V0h = V0h # multipatch Fem Space - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - u0s = [P(fun) for P, fun, in zip(self._P0s, funs_log)] - - u0_coeffs = BlockVector(self._V0h.coeff_space, - blocks=[u0j.coeffs for u0j in u0s]) - - return FemField(self._V0h, coeffs=u0_coeffs) - -# ============================================================================== - - -class Multipatch_Projector_Hcurl: - - """ - to apply the Hcurl projection (2D) on every patch - """ - - def __init__(self, V1h, nquads=None): - - self._P1s = [Projector_Hcurl(V, nquads=nquads) for V in V1h.spaces] - self._V1h = V1h # multipatch Fem Space - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - E1s = [P(fun) for P, fun, in zip(self._P1s, funs_log)] - - E1_coeffs = BlockVector(self._V1h.coeff_space, - blocks=[E1j.coeffs for E1j in E1s]) - - return FemField(self._V1h, coeffs=E1_coeffs) - -# ============================================================================== - - -class Multipatch_Projector_L2: - - """ - to apply the L2 projection (2D) on every patch - """ - - def __init__(self, V2h, nquads=None): - - self._P2s = [Projector_L2(V, nquads=nquads) for V in V2h.spaces] - self._V2h = V2h # multipatch Fem Space - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - B2s = [P(fun) for P, fun, in zip(self._P2s, funs_log)] - - B2_coeffs = BlockVector(self._V2h.coeff_space, - blocks=[B2j.coeffs for B2j in B2s]) - - return FemField(self._V2h, coeffs=B2_coeffs) diff --git a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py index bb1b09004..3986aabb2 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -1,13 +1,13 @@ # coding: utf-8 import numpy as np +import pytest from psydac.feec.multipatch.examples.hcurl_source_pbms_conga_2d import solve_hcurl_source_pbm from psydac.feec.multipatch.examples.hcurl_eigen_pbms_conga_2d import hcurl_solve_eigen_pbm from psydac.feec.multipatch.examples.hcurl_eigen_pbms_dg_2d import hcurl_solve_eigen_pbm_dg from psydac.feec.multipatch.examples.timedomain_maxwell import solve_td_maxwell_pbm - def test_time_harmonic_maxwell_pretzel_f(): nc = 4 deg = 2 @@ -29,8 +29,7 @@ def test_time_harmonic_maxwell_pretzel_f(): source_proj=source_proj, backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.007201508128407582) < 1e-10 - + assert abs(diags["err"] - 0.0072015081402929445) < 1e-10 def test_time_harmonic_maxwell_pretzel_f_nc(): deg = 2 @@ -54,8 +53,7 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): source_proj=source_proj, backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.004849165663310541) < 1e-10 - + assert abs(diags["err"] - 0.004849225522124346) < 5e-7 def test_maxwell_eigen_curved_L_shape(): domain_name = 'curved_L_shape' @@ -88,7 +86,6 @@ def test_maxwell_eigen_curved_L_shape(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell', ) error = 0 @@ -97,8 +94,7 @@ def test_maxwell_eigen_curved_L_shape(): error += (eigenvalues[k] - ref_sigmas[k])**2 error = np.sqrt(error) - assert abs(error - 0.01291539899483907) < 1e-10 - + assert abs(error - 0.012915398994855902) < 1e-10 def test_maxwell_eigen_curved_L_shape_nc(): domain_name = 'curved_L_shape' @@ -133,7 +129,6 @@ def test_maxwell_eigen_curved_L_shape_nc(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell_nc', ) error = 0 @@ -142,8 +137,7 @@ def test_maxwell_eigen_curved_L_shape_nc(): error += (eigenvalues[k] - ref_sigmas[k])**2 error = np.sqrt(error) - assert abs(error - 0.010504876643873904) < 1e-10 - + assert abs(error - 0.010504876643886937) < 1e-10 def test_maxwell_eigen_curved_L_shape_dg(): domain_name = 'curved_L_shape' @@ -176,7 +170,6 @@ def test_maxwell_eigen_curved_L_shape_dg(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell_dg', ) error = 0 @@ -184,9 +177,8 @@ def test_maxwell_eigen_curved_L_shape_dg(): for k in range(n_errs): error += (eigenvalues[k] - ref_sigmas[k])**2 error = np.sqrt(error) - - assert abs(error - 0.035139029534570064) < 1e-10 + assert abs(error - 0.035139029534592255) < 1e-10 def test_maxwell_timedomain(): solve_td_maxwell_pbm(nc = 4, deg = 2, final_time = 2, domain_name = 'square_2') diff --git a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py index 7a0d94cbb..2804fba2d 100644 --- a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -2,7 +2,6 @@ from psydac.feec.multipatch.examples.h1_source_pbms_conga_2d import solve_h1_source_pbm - def test_poisson_pretzel_f(): source_type = 'manu_poisson_2' @@ -19,7 +18,7 @@ def test_poisson_pretzel_f(): backend_language='pyccel-gcc', plot_dir=None) - assert abs(l2_error - 1.0585687717792318e-05) < 1e-10 + assert abs(l2_error - 1.1016888403643595e-05) < 5e-8 def test_poisson_pretzel_f_nc(): @@ -39,7 +38,7 @@ def test_poisson_pretzel_f_nc(): backend_language='pyccel-gcc', plot_dir=None) - assert abs(l2_error - 6.051557012306659e-06) < 1e-10 + assert abs(l2_error - 7.079666478120528e-06) < 5e-8 # ============================================================================== diff --git a/psydac/feec/multipatch/utils_conga_2d.py b/psydac/feec/multipatch/utils_conga_2d.py index 351511d5e..b457f0232 100644 --- a/psydac/feec/multipatch/utils_conga_2d.py +++ b/psydac/feec/multipatch/utils_conga_2d.py @@ -5,70 +5,41 @@ from sympy import lambdify from sympde.topology import Derham +from sympde.topology.callable_mapping import BasicCallableMapping from psydac.api.settings import PSYDAC_BACKENDS from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_l2 -from psydac.feec.multipatch.api import discretize -from psydac.feec.multipatch.utilities import time_count # , export_sol, import_sol +from psydac.api.discretization import discretize +from psydac.feec.multipatch.utilities import time_count from psydac.linalg.utilities import array_to_psydac from psydac.fem.basic import FemField from psydac.fem.plotting_utilities import get_plotting_grid, get_grid_quad_weights, get_grid_vals +from scipy.sparse import kron, block_diag +from psydac.core.bsplines import collocation_matrix, histopolation_matrix +from psydac.linalg.solvers import inverse -# commuting projections on the physical domain (should probably be in the -# interface) -def P0_phys(f_phys, P0, domain, mappings_list): - f = lambdify(domain.coordinates, f_phys) - f_log = [pull_2d_h1(f, m.get_callable_mapping()) for m in mappings_list] - return P0(f_log) - - -def P1_phys(f_phys, P1, domain, mappings_list): - f_x = lambdify(domain.coordinates, f_phys[0]) - f_y = lambdify(domain.coordinates, f_phys[1]) - f_log = [pull_2d_hcurl([f_x, f_y], m.get_callable_mapping()) - for m in mappings_list] - return P1(f_log) - - -def P2_phys(f_phys, P2, domain, mappings_list): - f = lambdify(domain.coordinates, f_phys) - f_log = [pull_2d_l2(f, m.get_callable_mapping()) for m in mappings_list] - return P2(f_log) # commuting projections on the physical domain (should probably be in the # interface) - - -def P_phys_h1(f_phys, P0, domain, mappings_list): +def P0_phys(f_phys, P0, domain): f = lambdify(domain.coordinates, f_phys) - if len(mappings_list) == 1: - m = mappings_list[0] - f_log = pull_2d_h1(f, m) - else: - f_log = [pull_2d_h1(f, m) for m in mappings_list] - return P0(f_log) + return P0(f) -def P_phys_hcurl(f_phys, P1, domain, mappings_list): - f_x = lambdify(domain.coordinates, f_phys[0]) - f_y = lambdify(domain.coordinates, f_phys[1]) - f_log = [pull_2d_hcurl([f_x, f_y], m) for m in mappings_list] - return P1(f_log) - -def P_phys_hdiv(f_phys, P1, domain, mappings_list): +def P1_phys(f_phys, P1, domain): f_x = lambdify(domain.coordinates, f_phys[0]) f_y = lambdify(domain.coordinates, f_phys[1]) - f_log = [pull_2d_hdiv([f_x, f_y], m) for m in mappings_list] - return P1(f_log) + + return P1([f_x, f_y]) -def P_phys_l2(f_phys, P2, domain, mappings_list): +def P2_phys(f_phys, P2, domain): f = lambdify(domain.coordinates, f_phys) - f_log = [pull_2d_l2(f, m) for m in mappings_list] - return P2(f_log) + + return P2(f) def get_kind(space='V*'): @@ -83,6 +54,142 @@ def get_kind(space='V*'): raise ValueError(space) return kind +# =============================================================================== +def get_K0_and_K0_inv(V0h, uniform_patches=False): + """ + Compute the change of basis matrices K0 and K0^{-1} in V0h. + + With + K0_ij = sigma^0_i(B_j) = B_jx(n_ix) * B_jy(n_iy) + where sigma_i is the geometric (interpolation) dof + and B_j is the tensor-product B-spline + """ + if uniform_patches: + print(' [[WARNING -- hack in get_K0_and_K0_inv: using copies of 1st-patch matrices in every patch ]] ') + + V0 = V0h.symbolic_space # VOh is FemSpace + domain = V0.domain + K0_blocks = [] + K0_inv_blocks = [] + for k, D in enumerate(domain.interior): + if uniform_patches and k > 0: + K0_k = K0_blocks[0].copy() + K0_inv_k = K0_inv_blocks[0].copy() + + else: + V0_k = V0h.spaces[k] # fem space on patch k: (TensorFemSpace) + K0_k_factors = [None, None] + for d in [0, 1]: + # 1d fem space alond dim d (SplineSpace) + V0_kd = V0_k.spaces[d] + K0_k_factors[d] = collocation_matrix( + knots=V0_kd.knots, + degree=V0_kd.degree, + periodic=V0_kd.periodic, + normalization=V0_kd.basis, + xgrid=V0_kd.greville + ) + K0_k = kron(*K0_k_factors) + K0_k.eliminate_zeros() + K0_inv_k = inv(K0_k.tocsc()) + K0_inv_k.eliminate_zeros() + + K0_blocks.append(K0_k) + K0_inv_blocks.append(K0_inv_k) + K0 = block_diag(K0_blocks) + K0_inv = block_diag(K0_inv_blocks) + return K0, K0_inv + + +# =============================================================================== +def get_K1_and_K1_inv(V1h, uniform_patches=False): + """ + Compute the change of basis matrices K1 and K1^{-1} in Hcurl space V1h. + + With + K1_ij = sigma^1_i(B_j) = int_{e_ix}(M_jx) * B_jy(n_iy) + if i = horizontal edge [e_ix, n_iy] and j = (M_jx o B_jy) x-oriented MoB spline + or + = B_jx(n_ix) * int_{e_iy}(M_jy) + if i = vertical edge [n_ix, e_iy] and j = (B_jx o M_jy) y-oriented BoM spline + (above, 'o' denotes tensor-product for functions) + """ + if uniform_patches: + print(' [[WARNING -- hack in get_K1_and_K1_inv: using copies of 1st-patch matrices in every patch ]] ') + + V1 = V1h.symbolic_space # V1h is FemSpace + domain = V1.domain + K1_blocks = [] + K1_inv_blocks = [] + for k, D in enumerate(domain.interior): + if uniform_patches and k > 0: + K1_k = K1_blocks[0].copy() + K1_inv_k = K1_inv_blocks[0].copy() + + else: + # fem space on patch k: + V1_k = V1h.spaces[k] + K1_k_blocks = [] + for c in [0, 1]: # dim of component + # fem space for comp. dc (TensorFemSpace) + V1_kc = V1_k.spaces[c] + K1_kc_factors = [None, None] + for d in [0, 1]: # dim of variable + # 1d fem space for comp c alond dim d (SplineSpace) + V1_kcd = V1_kc.spaces[d] + if c == d: + K1_kc_factors[d] = histopolation_matrix( + knots=V1_kcd.knots, + degree=V1_kcd.degree, + periodic=V1_kcd.periodic, + normalization=V1_kcd.basis, + xgrid=V1_kcd.ext_greville + ) + else: + K1_kc_factors[d] = collocation_matrix( + knots=V1_kcd.knots, + degree=V1_kcd.degree, + periodic=V1_kcd.periodic, + normalization=V1_kcd.basis, + xgrid=V1_kcd.greville + ) + K1_kc = kron(*K1_kc_factors) + K1_kc.eliminate_zeros() + K1_k_blocks.append(K1_kc) + K1_k = block_diag(K1_k_blocks) + K1_k.eliminate_zeros() + K1_inv_k = inv(K1_k.tocsc()) + K1_inv_k.eliminate_zeros() + + K1_blocks.append(K1_k) + K1_inv_blocks.append(K1_inv_k) + + K1 = block_diag(K1_blocks) + K1_inv = block_diag(K1_inv_blocks) + return K1, K1_inv + +# =============================================================================== + + +def ortho_proj_Hcurl(EE, V1h, domain_h, M1, backend_language='python'): + """ + return orthogonal projection of E on V1h, given M1 the mass matrix + """ + assert isinstance(EE, Tuple) + V1 = V1h.symbolic_space + v = element_of(V1, name='v') + l = LinearForm(v, integral(V1.domain, dot(v, EE))) + lh = discretize( + l, + domain_h, + V1h, + backend=PSYDAC_BACKENDS[backend_language]) + b = lh.assemble() + M1_inv = inverse(M1.mat(), 'pcg', pc='jacobi', tol=1e-10) + sol_coeffs = M1_inv @ b + + return FemField(V1h, coeffs=sol_coeffs) + # =============================================================================== class DiagGrid(): diff --git a/psydac/feec/tests/test_axis_projection.py b/psydac/feec/tests/test_axis_projection.py index 61e1dfb4f..7a81f0953 100644 --- a/psydac/feec/tests/test_axis_projection.py +++ b/psydac/feec/tests/test_axis_projection.py @@ -1,6 +1,6 @@ -from sympde.topology import Square, Derham, element_of -from sympde.expr.expr import BilinearForm, integral -from psydac.feec.multipatch.api import discretize +from sympde.topology import Square, Derham, element_of +from sympde.expr.expr import BilinearForm, integral +from psydac.api.discretization import discretize from psydac.api.settings import PSYDAC_BACKENDS def test_axis_projection(): diff --git a/psydac/feec/tests/test_commuting_projections.py b/psydac/feec/tests/test_commuting_projections.py index 38881220f..b038fbc25 100644 --- a/psydac/feec/tests/test_commuting_projections.py +++ b/psydac/feec/tests/test_commuting_projections.py @@ -3,13 +3,17 @@ import numpy as np import pytest -from psydac.feec.global_projectors import Projector_H1, Projector_L2, Projector_Hcurl, Projector_Hdiv +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorL2 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHcurl +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHdiv + from psydac.fem.tensor import TensorFemSpace, SplineSpace from psydac.fem.vector import VectorFemSpace from psydac.core.bsplines import make_knots -from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D -from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D -from psydac.feec.derivatives import Divergence_2D, Divergence_3D +from psydac.feec.derivatives import Derivative1D, Gradient2D, Gradient3D +from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D +from psydac.feec.derivatives import Divergence2D, Divergence3D from psydac.ddm.cart import DomainDecomposition from psydac.linalg.solvers import inverse from psydac.linalg.basic import IdentityOperator @@ -57,13 +61,13 @@ def test_3d_commuting_pro_1(Nel, Nq, p, bc, m): Hcurl = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = Projector_H1(H1) + P0 = GlobalGeometricProjectorH1(H1) # Build linear operators on stencil arrays - grad = Gradient_3D(H1, Hcurl) + grad = Gradient3D(H1, Hcurl) # create an instance of the projector class - P1 = Projector_Hcurl(Hcurl, Nq) + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -150,11 +154,11 @@ def test_3d_commuting_pro_2(Nel, Nq, p, bc, m): Hdiv = VectorFemSpace(*spaces) # Build linear operators on stencil arrays - curl = Curl_3D(Hcurl, Hdiv) + curl = Curl3D(Hcurl, Hdiv) # create an instance of the projector class - P1 = Projector_Hcurl(Hcurl, Nq) - P2 = Projector_Hdiv(Hdiv, Nq) + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) #------------------------------------- # Projections and discrete derivatives @@ -232,11 +236,11 @@ def test_3d_commuting_pro_3(Nel, Nq, p, bc, m): # create an instance of the H1 projector class # Build linear operators on stencil arrays - div = Divergence_3D(Hdiv, L2) + div = Divergence3D(Hdiv, L2) # create an instance of the projector class - P2 = Projector_Hdiv(Hdiv, Nq) - P3 = Projector_L2(L2, Nq) + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) + P3 = GlobalGeometricProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -307,13 +311,13 @@ def test_2d_commuting_pro_1(Nel, Nq, p, bc, m): Hcurl = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = Projector_H1(H1) + P0 = GlobalGeometricProjectorH1(H1) # Build linear operators on stencil arrays - grad = Gradient_2D(H1, Hcurl) + grad = Gradient2D(H1, Hcurl) # create an instance of the projector class - P1 = Projector_Hcurl(Hcurl, Nq) + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -380,13 +384,13 @@ def test_2d_commuting_pro_2(Nel, Nq, p, bc, m): Hdiv = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = Projector_H1(H1) + P0 = GlobalGeometricProjectorH1(H1) # Linear operator: 2D vector curl - curl = VectorCurl_2D(H1, Hdiv) + curl = VectorCurl2D(H1, Hdiv) # create an instance of the projector class - P1 = Projector_Hdiv(Hdiv, Nq) + P1 = GlobalGeometricProjectorHdiv(Hdiv, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -461,11 +465,11 @@ def test_2d_commuting_pro_3(Nel, Nq, p, bc, m): # create an instance of the H1 projector class # Build linear operators on stencil arrays - div = Divergence_2D(Hdiv, L2) + div = Divergence2D(Hdiv, L2) # create an instance of the projector class - P2 = Projector_Hdiv(Hdiv, Nq) - P3 = Projector_L2(L2, Nq) + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) + P3 = GlobalGeometricProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -541,11 +545,11 @@ def test_2d_commuting_pro_4(Nel, Nq, p, bc, m): # create an instance of the H1 projector class # Build linear operators on stencil arrays - curl = ScalarCurl_2D(Hcurl, L2) + curl = ScalarCurl2D(Hcurl, L2) # create an instance of the projector class - P1 = Projector_Hcurl(Hcurl, Nq) - P2 = Projector_L2(L2, Nq) + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) + P2 = GlobalGeometricProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -610,13 +614,13 @@ def test_1d_commuting_pro_1(Nel, Nq, p, bc, m): L2 = H1.reduce_degree(axes=[0], basis='M') # create an instance of the H1 projector class - P0 = Projector_H1(H1) + P0 = GlobalGeometricProjectorH1(H1) # Build linear operators on stencil arrays - grad = Derivative_1D(H1, L2) + grad = Derivative1D(H1, L2) # create an instance of the projector class - P1 = Projector_L2(L2, Nq) + P1 = GlobalGeometricProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- diff --git a/psydac/feec/tests/test_commuting_projections_dual.py b/psydac/feec/tests/test_commuting_projections_dual.py index 944915bc4..9057ba1ff 100644 --- a/psydac/feec/tests/test_commuting_projections_dual.py +++ b/psydac/feec/tests/test_commuting_projections_dual.py @@ -1,6 +1,6 @@ -from psydac.feec.derivatives import Gradient_3D -from psydac.feec.derivatives import Curl_3D -from psydac.feec.derivatives import Divergence_3D +from psydac.feec.derivatives import Gradient3D +from psydac.feec.derivatives import Curl3D +from psydac.feec.derivatives import Divergence3D from sympde.expr import LinearForm, integral from sympde.topology import Derham, element_of, Cube from psydac.api.discretization import discretize @@ -39,7 +39,7 @@ def test_transpose_div_3d(Nel, Nq, p, bc, m): v2 = element_of(derham.V2, name='v2') v3 = element_of(derham.V3, name='v3') - div = Divergence_3D(derham_h.V2, derham_h.V3) + div = Divergence3D(derham_h.V2, derham_h.V3) f2 = LinearForm(v2, integral(domain, D1fun1(*domain.coordinates)*v2[0] + D2fun1(*domain.coordinates)*v2[1] + D3fun1(*domain.coordinates)*v2[2])) f3 = LinearForm(v3, integral(domain, fun1(*domain.coordinates) * v3)) @@ -47,7 +47,7 @@ def test_transpose_div_3d(Nel, Nq, p, bc, m): u2 = discretize(f2, domain_h, derham_h.V2, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() u3 = discretize(f3, domain_h, derham_h.V3, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() - divT_u3 = - div.matrix.T.dot(u3) + divT_u3 = - div.linop.T.dot(u3) error = abs((u2-divT_u3).toarray()).max() assert error < 2e-10 @@ -97,7 +97,7 @@ def test_transpose_curl_3d(Nel, Nq, p, bc, m): v1 = element_of(derham.V1, name='v1') v2 = element_of(derham.V2, name='v2') - curl = Curl_3D(derham_h.V1, derham_h.V2) + curl = Curl3D(derham_h.V1, derham_h.V2) f1 = LinearForm(v1, integral(domain, cf1(*domain.coordinates)*v1[0] + cf2(*domain.coordinates)*v1[1] + cf3(*domain.coordinates)*v1[2])) f2 = LinearForm(v2, integral(domain, fun1(*domain.coordinates)*v2[0] + fun2(*domain.coordinates)*v2[1] + fun3(*domain.coordinates)*v2[2])) @@ -105,7 +105,7 @@ def test_transpose_curl_3d(Nel, Nq, p, bc, m): u1 = discretize(f1, domain_h, derham_h.V1, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() u2 = discretize(f2, domain_h, derham_h.V2, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() - curlT_u2 = curl.matrix.T.dot(u2) + curlT_u2 = curl.linop.T.dot(u2) error = abs((u1-curlT_u2).toarray()).max() assert error < 2e-9 @@ -144,7 +144,7 @@ def test_transpose_grad_3d(Nel, Nq, p, bc, m): v0 = element_of(derham.V0, name='v0') v1 = element_of(derham.V1, name='v1') - grad = Gradient_3D(derham_h.V0, derham_h.V1) + grad = Gradient3D(derham_h.V0, derham_h.V1) f0 = LinearForm(v0, integral(domain, (D1fun1(*domain.coordinates) + D2fun2(*domain.coordinates) + D3fun3(*domain.coordinates))*v0)) f1 = LinearForm(v1, integral(domain, fun1(*domain.coordinates)*v1[0] + fun2(*domain.coordinates)*v1[1] + fun3(*domain.coordinates)*v1[2])) @@ -152,7 +152,7 @@ def test_transpose_grad_3d(Nel, Nq, p, bc, m): u0 = discretize(f0, domain_h, derham_h.V0, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() u1 = discretize(f1, domain_h, derham_h.V1, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() - gradT_u1 = -grad.matrix.T.dot(u1) + gradT_u1 = -grad.linop.T.dot(u1) error = abs((u0-gradT_u1).toarray()).max() assert error < 5e-10 diff --git a/psydac/feec/tests/test_differentiation_matrices.py b/psydac/feec/tests/test_differentiation_matrices.py index a60cbae36..609799dd3 100644 --- a/psydac/feec/tests/test_differentiation_matrices.py +++ b/psydac/feec/tests/test_differentiation_matrices.py @@ -8,11 +8,9 @@ from psydac.fem.vector import VectorFemSpace from psydac.feec.derivatives import DirectionalDerivativeOperator -from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D -from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D -from psydac.feec.derivatives import Divergence_2D, Divergence_3D - -from psydac.feec.global_projectors import Projector_H1 +from psydac.feec.derivatives import Derivative1D, Gradient2D, Gradient3D +from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D +from psydac.feec.derivatives import Divergence2D, Divergence3D from psydac.ddm.cart import DomainDecomposition from mpi4py import MPI @@ -352,7 +350,7 @@ def test_directional_derivative_operator_3d_par(domain, ncells, degree, periodic @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [1,2]) -def test_Derivative_1D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Derivative1D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -372,7 +370,7 @@ def test_Derivative_1D(domain, ncells, degree, periodic, seed, multiplicity): u0 = FemField(V0) # Linear operator: 1D derivative - grad = Derivative_1D(V0, V1) + grad = Derivative1D(V0, V1) # Create random field in V0 s, = V0.coeff_space.starts @@ -401,7 +399,7 @@ def test_Derivative_1D(domain, ncells, degree, periodic, seed, multiplicity): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) -def test_Gradient_2D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Gradient2D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -421,7 +419,7 @@ def test_Gradient_2D(domain, ncells, degree, periodic, seed, multiplicity): V1 = VectorFemSpace(DxNy, NxDy) # Linear operator: 2D gradient - grad = Gradient_2D(V0, V1) + grad = Gradient2D(V0, V1) # Create random field in V0 u0 = FemField(V0) @@ -463,7 +461,7 @@ def test_Gradient_2D(domain, ncells, degree, periodic, seed, multiplicity): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1, 1), (1, 2, 2), (2, 2, 2)]) -def test_Gradient_3D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Gradient3D(domain, ncells, degree, periodic, seed, multiplicity): if any([ncells[d] <= degree[d] and periodic[d] for d in range(3)]): return @@ -489,7 +487,7 @@ def test_Gradient_3D(domain, ncells, degree, periodic, seed, multiplicity): V1 = VectorFemSpace(DxNyNz, NxDyNz, NxNyDz) # Linear operator: 3D gradient - grad = Gradient_3D(V0, V1) + grad = Gradient3D(V0, V1) # Create random field in V0 u0 = FemField(V0) @@ -530,7 +528,7 @@ def test_Gradient_3D(domain, ncells, degree, periodic, seed, multiplicity): @pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) -def test_ScalarCurl_2D(domain, ncells, degree, periodic, seed, multiplicity): +def test_ScalarCurl2D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -555,7 +553,7 @@ def test_ScalarCurl_2D(domain, ncells, degree, periodic, seed, multiplicity): V2 = DxDy # Linear operator: curl - curl = ScalarCurl_2D(V1, V2) + curl = ScalarCurl2D(V1, V2) # ... # Create random field in V1 @@ -605,7 +603,7 @@ def eval_curl(fx, fy, *eta): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) -def test_VectorCurl_2D(domain, ncells, degree, periodic, seed, multiplicity): +def test_VectorCurl2D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -625,7 +623,7 @@ def test_VectorCurl_2D(domain, ncells, degree, periodic, seed, multiplicity): V1 = VectorFemSpace(NxDy, DxNy) # Linear operator: 2D vector curl - curl = VectorCurl_2D(V0, V1) + curl = VectorCurl2D(V0, V1) # Create random field in V0 u0 = FemField(V0) @@ -671,7 +669,7 @@ def eval_curl(f, *eta): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1, 1), (1, 2, 2), (2, 2, 2)]) -def test_Curl_3D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Curl3D(domain, ncells, degree, periodic, seed, multiplicity): if any([ncells[d] <= degree[d] and periodic[d] for d in range(3)]): return @@ -703,7 +701,7 @@ def test_Curl_3D(domain, ncells, degree, periodic, seed, multiplicity): V2 = VectorFemSpace(NxDyDz, DxNyDz, DxDyNz) # Linear operator: curl - curl = Curl_3D(V1, V2) + curl = Curl3D(V1, V2) # ... # Create random field in V1 @@ -763,7 +761,7 @@ def eval_curl(fx, fy, fz, *eta): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) -def test_Divergence_2D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Divergence2D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -787,7 +785,7 @@ def test_Divergence_2D(domain, ncells, degree, periodic, seed, multiplicity): V2 = V0.reduce_degree(axes=[0, 1], basis='M') # Linear operator: divergence - div = Divergence_2D(V1, V2) + div = Divergence2D(V1, V2) # ... # Create random field in V1 @@ -839,7 +837,7 @@ def eval_div(fx, fy, *eta): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1, 1), (1, 2, 2), (2, 2, 2)]) -def test_Divergence_3D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Divergence3D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -865,7 +863,7 @@ def test_Divergence_3D(domain, ncells, degree, periodic, seed, multiplicity): V3 = V0.reduce_degree(axes=[0, 1, 2], basis='M') # Linear operator: divergence - div = Divergence_3D(V2, V3) + div = Divergence3D(V2, V3) # ... # Create random field in V2 @@ -915,11 +913,11 @@ def eval_div(fx, fy, fz, *eta): #============================================================================== if __name__ == '__main__': - test_Derivative_1D(domain=[0, 1], ncells=3, degree=3, periodic=False, seed=1, multiplicity=1) - test_Derivative_1D(domain=[0, 1], ncells=12, degree=3, periodic=True, seed=1, multiplicity=1) + test_Derivative1D(domain=[0, 1], ncells=3, degree=3, periodic=False, seed=1, multiplicity=1) + test_Derivative1D(domain=[0, 1], ncells=12, degree=3, periodic=True, seed=1, multiplicity=1) - test_Gradient_2D( + test_Gradient2D( domain = ([0, 1], [0, 1]), ncells = (10, 15), degree = (3, 2), @@ -927,7 +925,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_Gradient_3D( + test_Gradient3D( domain = ([0, 1], [0, 1], [0, 1]), ncells = (5, 8, 4), degree = (3, 2, 3), @@ -935,7 +933,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_ScalarCurl_2D( + test_ScalarCurl2D( domain = ([0, 1], [0, 1]), ncells = (10, 15), degree = (3, 2), @@ -943,7 +941,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_VectorCurl_2D( + test_VectorCurl2D( domain = ([0, 1], [0, 1]), ncells = (10, 15), degree = (3, 2), @@ -951,7 +949,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_Curl_3D( + test_Curl3D( domain = ([0, 1], [0, 1], [0, 1]), ncells = (5, 8, 4), degree = (3, 2, 3), @@ -959,7 +957,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_Divergence_2D( + test_Divergence2D( domain = ([0, 1], [0, 1]), ncells = (10, 15), degree = (3, 2), @@ -967,7 +965,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_Divergence_3D( + test_Divergence3D( domain = ([0, 1], [0, 1], [0, 1]), ncells = (5, 8, 4), degree = (3, 2, 3), diff --git a/psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py similarity index 54% rename from psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py rename to psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index 3d0dd85ff..01a0dd38e 100644 --- a/psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -2,16 +2,16 @@ from collections import OrderedDict import numpy as np -from sympy import Tuple +from sympy import Tuple, lambdify from scipy.sparse.linalg import norm as sp_norm +from scipy.sparse.linalg import inv from sympde.topology.domain import Domain from sympde.topology import Derham, Square, IdentityMapping -from psydac.feec.multipatch.api import discretize -from psydac.feec.multipatch.operators import HodgeOperator -from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection -from psydac.feec.multipatch.utils_conga_2d import P_phys_l2, P_phys_hdiv, P_phys_hcurl, P_phys_h1 +from psydac.api.discretization import discretize + +from psydac.fem.projectors import get_dual_dofs def get_polynomial_function(degree, hom_bc_axes, domain): @@ -37,7 +37,10 @@ def get_polynomial_function(degree, hom_bc_axes, domain): # else: g0_y = (y - 0.75)**degree[1] - return g0_x * g0_y + expr = g0_x * g0_y + callable_function = lambdify(domain.coordinates, expr) + + return expr, callable_function # ============================================================================== @@ -46,10 +49,10 @@ def get_polynomial_function(degree, hom_bc_axes, domain): @pytest.mark.parametrize('nc', [5]) @pytest.mark.parametrize('reg', [0]) @pytest.mark.parametrize('hom_bc', [False, True]) -@pytest.mark.parametrize('domain_name', ["4patch_nc", "2patch_nc"]) +@pytest.mark.parametrize('domain_name', ["1patch", "4patch_nc", "2patch_nc"]) @pytest.mark.parametrize("nonconforming, full_mom_pres", [(True, True), (False, True)]) -# NOTE (MCP march 2025): momentum conservation fails for nc = 4 and degree = 3, why? + def test_conf_projectors_2d( V1_type, degree, @@ -60,8 +63,12 @@ def test_conf_projectors_2d( domain_name, nonconforming ): + if domain_name == '1patch': + log_domain = Square('Omega', bounds1=(0, 1), bounds2=(0, 1)) + mapping = IdentityMapping('M1', dim=2) + domain = mapping(log_domain) - if domain_name == '2patch_nc': + elif domain_name == '2patch_nc': A = Square('A', bounds1=(0, 0.5), bounds2=(0, 1)) B = Square('B', bounds1=(0.5, 1.), bounds2=(0, 1)) @@ -95,8 +102,10 @@ def test_conf_projectors_2d( ((0, 1, 1), (2, 1, -1), 1), ((1, 1, 1), (3, 1, -1), 1)], name='domain') + if domain_name == '1patch': + ncells_h = {domain.name: [nc, nc]} - if nonconforming: + elif nonconforming: if len(domain) == 2: ncells_h = { 'M1(A)': [nc, nc], @@ -115,24 +124,14 @@ def test_conf_projectors_2d( for k, D in enumerate(domain.interior): ncells_h[D.name] = [nc, nc] - derham = Derham(domain, ["H1", "Hcurl", "L2"]) domain_h = discretize(domain, ncells=ncells_h) # Vh space - derham_h = discretize(derham, domain_h, degree=degree) - V0h = derham_h.V0 - V1h = derham_h.V1 - V2h = derham_h.V2 - - mappings = OrderedDict([(P.logical_domain, P.mapping) - for P in domain.interior]) - mappings_list = [m.get_callable_mapping() for m in mappings.values()] - p_derham = Derham(domain, ["H1", V1_type, "L2"]) + derham = Derham(domain, ["H1", V1_type, "L2"]) nquads = [(d + 1) for d in degree] - p_derham_h = discretize(p_derham, domain_h, degree=degree) - p_V0h = p_derham_h.V0 - p_V1h = p_derham_h.V1 - p_V2h = p_derham_h.V2 + derham_h = discretize(derham, domain_h, degree=degree) + V0h, V1h, V2h = derham_h.spaces + # full moment preservation only possible if enough interior functions in a # patch (<=> enough cells) @@ -145,38 +144,26 @@ def test_conf_projectors_2d( # moment preservation... # geometric projections (operators) - p_geomP0, p_geomP1, p_geomP2 = p_derham_h.projectors(nquads=nquads) + geomP0, geomP1, geomP2 = derham_h.projectors(nquads=nquads) # conforming projections (scipy matrices) - cP0 = construct_h1_conforming_projection(V0h, reg, mom_pres, hom_bc) - cP1 = construct_hcurl_conforming_projection(V1h, reg, mom_pres, hom_bc) - cP2 = construct_h1_conforming_projection(V2h, reg - 1, mom_pres, hom_bc) + cP0, cP1, cP2 = derham_h.conforming_projectors(p_moments=mom_pres, hom_bc=hom_bc) + cP0, cP1, cP2 = (m.tosparse() for m in (cP0, cP1, cP2)) + + M0, M1, M2 = derham_h.hodge_operators() + M0, M1, M2 = (m.tosparse().tocsc() for m in (M0, M1, M2)) - HOp0 = HodgeOperator(p_V0h, domain_h) - M0 = HOp0.to_sparse_matrix() # mass matrix - M0_inv = HOp0.get_dual_Hodge_sparse_matrix() # inverse mass matrix + M0_inv, M1_inv, M2_inv = (inv(m) for m in (M0, M1, M2)) - HOp1 = HodgeOperator(p_V1h, domain_h) - M1 = HOp1.to_sparse_matrix() # mass matrix - M1_inv = HOp1.get_dual_Hodge_sparse_matrix() # inverse mass matrix + bD0, bD1 = derham_h.derivatives() + bD0, bD1 = (m.tosparse() for m in (bD0, bD1)) - HOp2 = HodgeOperator(p_V2h, domain_h) - M2 = HOp2.to_sparse_matrix() # mass matrix - M2_inv = HOp2.get_dual_Hodge_sparse_matrix() # inverse mass matrix - - bD0, bD1 = p_derham_h.broken_derivatives_as_operators - - bD0 = bD0.to_sparse_matrix() # broken grad - bD1 = bD1.to_sparse_matrix() # broken curl or div D0 = bD0 @ cP0 # Conga grad D1 = bD1 @ cP1 # Conga curl or div - assert np.allclose(sp_norm(cP0 - cP0 @ cP0), 0, 1e-12, - 1e-12) # cP0 is a projection - assert np.allclose(sp_norm(cP1 - cP1 @ cP1), 0, 1e-12, - 1e-12) # cP1 is a projection - assert np.allclose(sp_norm(cP2 - cP2 @ cP2), 0, 1e-12, - 1e-12) # cP2 is a projection + assert np.allclose(sp_norm(cP0 - cP0 @ cP0), 0, 1e-12, 1e-12) # cP0 is a projection + assert np.allclose(sp_norm(cP1 - cP1 @ cP1), 0, 1e-12, 1e-12) # cP1 is a projection + assert np.allclose(sp_norm(cP2 - cP2 @ cP2), 0, 1e-12, 1e-12) # cP2 is a projection # D0 maps in the conforming V1 space (where cP1 coincides with Id) assert np.allclose(sp_norm(D0 - cP1 @ D0), 0, 1e-12, 1e-12) @@ -186,14 +173,11 @@ def test_conf_projectors_2d( # comparing projections of polynomials which should be exact # tests on cP0: - g0 = get_polynomial_function( - degree=degree, hom_bc_axes=[ - hom_bc, hom_bc], domain=domain) - g0h = P_phys_h1(g0, p_geomP0, domain, mappings_list) + g0, g0_fun = get_polynomial_function(degree=degree, hom_bc_axes=[hom_bc, hom_bc], domain=domain) + g0h = geomP0(g0_fun) g0_c = g0h.coeffs.toarray() - tilde_g0_c = p_derham_h.get_dual_dofs( - space='V0', f=g0, return_format='numpy_array') + tilde_g0_c = get_dual_dofs(Vh=V0h, f=g0, domain_h = domain_h, return_format='numpy_array') g0_L2_c = M0_inv @ tilde_g0_c # (P0_geom - P0_L2) polynomial = 0 @@ -206,46 +190,27 @@ def test_conf_projectors_2d( # the following projection should be exact for polynomials of proper degree (no bc) # conf_P0* : L2 -> V0 defined by := # for all phi in V0 - g0 = get_polynomial_function(degree=degree, hom_bc_axes=[ - False, False], domain=domain) - g0h = P_phys_h1(g0, p_geomP0, domain, mappings_list) + g0, g0_fun = get_polynomial_function(degree=degree, hom_bc_axes=[False, False], domain=domain) + g0h = geomP0(g0_fun) g0_c = g0h.coeffs.toarray() - tilde_g0_c = p_derham_h.get_dual_dofs( - space='V0', f=g0, return_format='numpy_array') + tilde_g0_c = get_dual_dofs(Vh=V0h, f=g0, domain_h = domain_h, return_format='numpy_array') + g0_star_c = M0_inv @ cP0.transpose() @ tilde_g0_c # (P10_geom - P0_star) polynomial = 0 assert np.allclose(g0_c, g0_star_c, 1e-12, 1e-12) # tests on cP1: + G1_x, G1_x_fun = get_polynomial_function(degree=[degree[0] - 1,degree[1]], hom_bc_axes=[False, hom_bc], domain=domain) + G1_y, G1_y_fun = get_polynomial_function(degree=[degree[0], degree[1] - 1], hom_bc_axes=[hom_bc, False], domain=domain) + + G1 = Tuple(G1_x, G1_y) + G1_fun = [G1_x_fun, G1_y_fun] - G1 = Tuple( - get_polynomial_function( - degree=[ - degree[0] - 1, - degree[1]], - hom_bc_axes=[ - False, - hom_bc], - domain=domain), - get_polynomial_function( - degree=[ - degree[0], - degree[1] - 1], - hom_bc_axes=[ - hom_bc, - False], - domain=domain) - ) - - if V1_type == "Hcurl": - G1h = P_phys_hcurl(G1, p_geomP1, domain, mappings_list) - elif V1_type == "Hdiv": - G1h = P_phys_hdiv(G1, p_geomP1, domain, mappings_list) - + G1h = geomP1(G1_fun) G1_c = G1h.coeffs.toarray() - tilde_G1_c = p_derham_h.get_dual_dofs( - space='V1', f=G1, return_format='numpy_array') + tilde_G1_c = get_dual_dofs(Vh=V1h, f=G1, domain_h=domain_h, return_format='numpy_array') + G1_L2_c = M1_inv @ tilde_G1_c assert np.allclose(G1_c, G1_L2_c, 1e-12, 1e-12) @@ -254,48 +219,29 @@ def test_conf_projectors_2d( if full_mom_pres: # as above - G1 = Tuple( - get_polynomial_function( - degree=[ - degree[0] - 1, - degree[1]], - hom_bc_axes=[ - False, - False], - domain=domain), - get_polynomial_function( - degree=[ - degree[0], - degree[1] - 1], - hom_bc_axes=[ - False, - False], - domain=domain) - ) - - G1h = P_phys_hcurl(G1, p_geomP1, domain, mappings_list) - G1_c = G1h.coeffs.toarray() - - tilde_G1_c = p_derham_h.get_dual_dofs( - space='V1', f=G1, return_format='numpy_array') - G1_star_c = M1_inv @ cP1.transpose() @ tilde_G1_c - # (P1_geom - P1_star) polynomial = 0 - assert np.allclose(G1_c, G1_star_c, 1e-12, 1e-12) + G1_x, G1_x_fun = get_polynomial_function(degree=[degree[0] - 1,degree[1]], hom_bc_axes=[False, False], domain=domain) + G1_y, G1_y_fun = get_polynomial_function(degree=[degree[0], degree[1] - 1], hom_bc_axes=[False, False], domain=domain) + + G1 = Tuple(G1_x, G1_y) + G1_fun = [G1_x_fun, G1_y_fun] + + G1h = geomP1(G1_fun) + G1_c = G1h.coeffs.toarray() + + G1h = geomP1(G1_fun) + G1_c = G1h.coeffs.toarray() + + tilde_G1_c = get_dual_dofs(Vh=V1h, f=G1, domain_h=domain_h, return_format='numpy_array') + G1_star_c = M1_inv @ cP1.transpose() @ tilde_G1_c + # (P1_geom - P1_star) polynomial = 0 + assert np.allclose(G1_c, G1_star_c, 1e-12, 1e-12) # tests on cP2 (non trivial for reg = 1): - g2 = get_polynomial_function( - degree=[ - degree[0] - 1, - degree[1] - 1], - hom_bc_axes=[ - False, - False], - domain=domain) - g2h = P_phys_l2(g2, p_geomP2, domain, mappings_list) + g2, g2_fun = get_polynomial_function(degree=[degree[0] - 1, degree[1] - 1], hom_bc_axes=[False, False], domain=domain) + g2h = geomP2(g2_fun) g2_c = g2h.coeffs.toarray() - tilde_g2_c = p_derham_h.get_dual_dofs( - space='V2', f=g2, return_format='numpy_array') + tilde_g2_c = get_dual_dofs(Vh=V2h, f=g2, domain_h=domain_h, return_format='numpy_array') g2_L2_c = M2_inv @ tilde_g2_c # (P2_geom - P2_L2) polynomial = 0 @@ -305,7 +251,6 @@ def test_conf_projectors_2d( if full_mom_pres: # as above, here with same degree and bc as - # tilde_g2_c = p_derham_h.get_dual_dofs(space='V2', f=g2, return_format='numpy_array', nquads=nquads) g2_star_c = M2_inv @ cP2.transpose() @ tilde_g2_c # (P2_geom - P2_star) polynomial = 0 assert np.allclose(g2_c, g2_star_c, 1e-12, 1e-12) diff --git a/psydac/feec/tests/test_global_projectors.py b/psydac/feec/tests/test_global_projectors.py index 1d1f64282..c1389f17f 100644 --- a/psydac/feec/tests/test_global_projectors.py +++ b/psydac/feec/tests/test_global_projectors.py @@ -1,11 +1,13 @@ import numpy as np import pytest -from psydac.core.bsplines import make_knots -from psydac.fem.basic import FemField -from psydac.fem.splines import SplineSpace -from psydac.fem.tensor import TensorFemSpace -from psydac.feec.global_projectors import Projector_H1, Projector_L2 +from psydac.core.bsplines import make_knots +from psydac.fem.basic import FemField +from psydac.fem.splines import SplineSpace +from psydac.fem.tensor import TensorFemSpace +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorL2 + from psydac.ddm.cart import DomainDecomposition from sympde.topology import Square, Cube from psydac.api.discretization import discretize @@ -33,7 +35,7 @@ def test_H1_projector_1d(domain, ncells, degree, periodic, multiplicity): V0 = TensorFemSpace(domain_decomposition, N) # Projector onto H1 space (1D interpolation) - P0 = Projector_H1(V0) + P0 = GlobalGeometricProjectorH1(V0) # Function to project f = lambda xi1 : np.sin( xi1 + 0.5 ) @@ -79,7 +81,7 @@ def test_L2_projector_1d(domain, ncells, degree, periodic, nquads, multiplicity) V1 = V0.reduce_degree(axes=[0], basis='M') # Projector onto L2 space (1D histopolation) - P1 = Projector_L2(V1, nquads=[nquads]) + P1 = GlobalGeometricProjectorL2(V1, nquads=[nquads]) # Function to project f = lambda xi1 : np.sin( xi1 + 0.5 ) diff --git a/psydac/feec/tests/test_projections_parallel.py b/psydac/feec/tests/test_projections_parallel.py index 034287bd1..a4cad1cdc 100644 --- a/psydac/feec/tests/test_projections_parallel.py +++ b/psydac/feec/tests/test_projections_parallel.py @@ -9,9 +9,13 @@ from psydac.fem.splines import SplineSpace from psydac.fem.tensor import TensorFemSpace from psydac.fem.vector import VectorFemSpace -from psydac.feec.global_projectors import Projector_H1, Projector_L2, Projector_Hcurl, Projector_Hdiv from psydac.ddm.cart import DomainDecomposition +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorL2 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHcurl +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHdiv + def run_projection_comparison(domain, ncells, degree, periodic, funcs, reduce): @@ -19,45 +23,45 @@ def run_projection_comparison(domain, ncells, degree, periodic, funcs, reduce): if len(domain) == 1: if reduce == 0: opV = lambda V0: V0 - opP = Projector_H1 + opP = GlobalGeometricProjectorH1 else: opV = lambda V0: V0.reduce_degree(axes=[0], basis='M') - opP = Projector_L2 + opP = GlobalGeometricProjectorL2 elif len(domain) == 2: if reduce == 0: opV = lambda V0: V0 - opP = Projector_H1 + opP = GlobalGeometricProjectorH1 elif reduce == 1: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[0], basis='M'), V0.reduce_degree(axes=[1], basis='M')) - opP = Projector_Hcurl + opP = GlobalGeometricProjectorHcurl elif reduce == 2: # (note: this would be more instructive, if the index was 1 as well...) opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[1], basis='M'), V0.reduce_degree(axes=[0], basis='M')) - opP = Projector_Hdiv + opP = GlobalGeometricProjectorHdiv else: opV = lambda V0: V0.reduce_degree(axes=[0,1], basis='M') - opP = Projector_L2 + opP = GlobalGeometricProjectorL2 elif len(domain) == 3: if reduce == 0: opV = lambda V0: V0 - opP = Projector_H1 + opP = GlobalGeometricProjectorH1 elif reduce == 1: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[0], basis='M'), V0.reduce_degree(axes=[1], basis='M'), V0.reduce_degree(axes=[2], basis='M')) - opP = Projector_Hcurl + opP = GlobalGeometricProjectorHcurl elif reduce == 2: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[1,2], basis='M'), V0.reduce_degree(axes=[0,2], basis='M'), V0.reduce_degree(axes=[0,1], basis='M')) - opP = Projector_Hdiv + opP = GlobalGeometricProjectorHdiv else: opV = lambda V0: V0.reduce_degree(axes=[0,1,2], basis='M') - opP = Projector_L2 + opP = GlobalGeometricProjectorL2 # Choose number of quadrature points nquads = None if reduce == 0 else [d + 1 for d in degree] diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index 09a1466fb..090243205 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -7,9 +7,9 @@ """ from abc import ABCMeta, abstractmethod -from psydac.linalg.basic import Vector +from psydac.linalg.basic import Vector, LinearOperator -__all__ = ('FemSpace', 'FemField') +__all__ = ('FemSpace', 'FemField', 'FemLinearOperator') #=============================================================================== # ABSTRACT BASE CLASS: FINITE ELEMENT SPACE @@ -380,3 +380,86 @@ def __isub__(self, other): assert self._space is other._space self._coeffs -= other._coeffs return self + +#=============================================================================== +# CONCRETE CLASS: Linear Operator acting on a FEM field +#=============================================================================== +class FemLinearOperator: + """ + Linear operators with an additional FEM layer. + There is also a shorthand access to sparse matrices as they are sometimes + used in the FEEC interfaces. + Parameters + ---------- + fem_domain : psydac.fem.basic.FemSpace + The discrete space of the domain + + fem_codomain : psydac.fem.basic.FemSpace + The discrete space of the codomain + + linop : + Linear Operator. + + """ + + def __init__(self, fem_domain, fem_codomain, *, linop=None): + assert isinstance(fem_domain, FemSpace) + assert isinstance(fem_codomain, FemSpace) + if linop is not None: + assert isinstance(linop, LinearOperator) + + self._fem_domain = fem_domain + self._fem_codomain = fem_codomain + + self._linop_domain = fem_domain.coeff_space + self._linop_codomain = fem_codomain.coeff_space + + self._linop = linop + + @property + def fem_domain(self): + return self._fem_domain + + @property + def fem_codomain(self): + return self._fem_codomain + + @property + def linop_domain(self): + return self._linop_domain + + @property + def linop_codomain(self): + return self._linop_codomain + + @property + def linop(self): + return self._linop + + def toarray(self): + return self._linop.toarray() + + def tosparse(self): + return self._linop.tosparse() + + #-------------------------------------------------------------------------- + def __call__(self, u, *, out=None): + assert isinstance(u, FemField) + assert u.space == self.fem_domain + + if self._linop is not None: + coeffs = self._linop.dot(u.coeffs) + else: + raise NotImplementedError('Class does not provide a __call__ method without a linear operator') + + return FemField(self.fem_codomain, coeffs=coeffs) + + def dot(self, f_coeffs, *, out=None): + assert isinstance(f_coeffs, Vector) + assert f_coeffs.space is self._linop_domain + + if self._linop is not None: + f = FemField(self.fem_domain, coeffs=f_coeffs) + return self(f).coeffs + else: + raise NotImplementedError('Class does not provide a dot method without a linear operator') diff --git a/psydac/fem/projectors.py b/psydac/fem/projectors.py index d40db6a8c..e13185051 100644 --- a/psydac/fem/projectors.py +++ b/psydac/fem/projectors.py @@ -1,8 +1,17 @@ import numpy as np -from psydac.linalg.kron import KroneckerDenseMatrix -from psydac.core.bsplines import hrefinement_matrix -from psydac.linalg.stencil import StencilVectorSpace +from sympde.topology import element_of +from sympde.topology.space import ScalarFunction +from sympde.topology.mapping import Mapping +from sympde.calculus import dot +from sympde.expr.expr import LinearForm, integral + +from psydac.api.settings import PSYDAC_BACKENDS + +from psydac.linalg.kron import KroneckerDenseMatrix +from psydac.core.bsplines import hrefinement_matrix +from psydac.linalg.stencil import StencilVectorSpace +from psydac.fem.basic import FemSpace __all__ = ('knots_to_insert', 'knot_insertion_projection_operator') @@ -100,3 +109,52 @@ def knot_insertion_projection_operator(domain, codomain): ops.append(np.eye(d.nbasis)) return KroneckerDenseMatrix(domain.coeff_space, codomain.coeff_space, *ops) + + +def get_dual_dofs(Vh, f, domain_h, backend_language="python", return_format='stencil_array'): + """ + return the dual dofs tilde_sigma_i(f) = < Lambda_i, f >_{L2} i = 1, .. dim(Vh)) of a given function f, as a stencil array or numpy array + + Parameters + ---------- + Vh : FemSpace + The discrete space for the dual dofs + + f : + The function used for evaluation + + domain_h : + The discrete domain corresponding to Vh + + backend_language: + The backend used to accelerate the code + + return_format: + The format of the dofs, can be 'stencil_array' or 'numpy_array' + + Returns + ------- + tilde_f: + The dual dofs + """ + + from psydac.api.discretization import discretize + + assert isinstance(Vh, FemSpace) + + V = Vh.symbolic_space + v = element_of(V, name='v') + + if Vh.is_vector_valued: + expr = dot(f,v) + else: + expr = f*v + + l = LinearForm(v, integral( V.domain, expr)) + lh = discretize(l, domain_h, Vh, backend=PSYDAC_BACKENDS[backend_language]) + tilde_f = lh.assemble() + + if return_format == 'numpy_array': + return tilde_f.toarray() + else: + return tilde_f diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index a6a02f68a..ba8ca3663 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -720,11 +720,10 @@ def set_scalar(self, c): self._scalar = c def toarray(self): - return self._scalar*self._operator.toarray() + return self._scalar * self._operator.toarray() def tosparse(self): - from scipy.sparse import csr_matrix - return self._scalar*csr_matrix(self._operator.toarray()) + return self._scalar * self._operator.tosparse().tocsr() def transpose(self, conjugate=False): return ScaledLinearOperator(domain=self.codomain, codomain=self.domain, c=self._scalar if not conjugate else np.conjugate(self._scalar), A=self._operator.transpose(conjugate=conjugate)) @@ -1326,7 +1325,7 @@ def dot(self, v, out=None, **kwargs): self._dot(v, out=out, **kwargs) else: # provided dot product does not take an out argument: we simply copy the result into out - self._dot(v).copy(out=out, **kwargs) + self._dot(v, **kwargs).copy(out=out) return out diff --git a/psydac/linalg/sparse.py b/psydac/linalg/sparse.py new file mode 100644 index 000000000..71fa29fed --- /dev/null +++ b/psydac/linalg/sparse.py @@ -0,0 +1,114 @@ +# coding: utf-8 + +from scipy.sparse import sparray, csr_array, bsr_array +from scipy.sparse import spmatrix, csr_matrix, bsr_matrix + +from psydac.linalg.basic import LinearOperator +from psydac.linalg.basic import VectorSpace, Vector, LinearOperator +from psydac.linalg.stencil import StencilVector +from psydac.linalg.block import BlockVector + +__all__ = ( + 'SparseMatrixLinearOperator', +) + +class SparseMatrixLinearOperator(LinearOperator): + """ + LinearOperator representation of a sparse matrix. + + Parameters + ---------- + domain : VectorSpace + The domain of the operator. + + codomain : VectorSpace + The codomain of the operator. + + sparse_matrix : scipy.sparse.sparray | scipy.sparse.spmatrix + The sparse SciPy matrix representing the operator. Recommended formats are + CSR and BSR. Any other format will be converted to CSR (csr_array). + """ + + def __init__(self, domain, codomain, sparse_matrix): + + assert isinstance(domain, VectorSpace) + assert isinstance(codomain, VectorSpace) + assert isinstance(sparse_matrix, (sparray, spmatrix)) + + if not isinstance(sparse_matrix, + (csr_array, csr_matrix, + bsr_array, bsr_matrix)): + sparse_matrix = sparse_matrix.tocsr() + + if domain.parallel: + raise NotImplementedError('Parallel SparseMatrixLinearOperator not supported yet.') + + self._domain = domain + self._codomain = codomain + self._matrix = sparse_matrix + + @property + def domain(self): + return self._domain + + @property + def codomain(self): + return self._codomain + + @property + def dtype(self): + return self._matrix.dtype + + def toarray(self): + return self._matrix.toarray() + + def tosparse(self): + return self._matrix + + def transpose(self, conjugate=False): + if conjugate: + return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.getH().tocsr()) + else: + return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T.tocsr()) + + def dot(self, v, out=None): + assert isinstance(v, Vector) + assert v.space is self.domain + + if out is not None: + assert isinstance(out, Vector) + assert out.space is self.codomain + out *= 0 + else: + out = self.codomain.zeros() + + self._dot_recursive(v, out=out) + + return out + + def _dot_recursive(self, v, out, ind_V=0, ind_W=0): + V = v.space + W = out.space + + if isinstance(v, StencilVector): + index_global_W = tuple(slice(s, e+1) for s, e in zip(W.starts, W.ends)) + index_global_V = tuple(slice(s, e+1) for s, e in zip(V.starts, V.ends)) + + dim_W = W.dimension + dim_V = V.dimension + + out[index_global_W].flat += self._matrix[ind_W:ind_W+dim_W, ind_V:ind_V+dim_V] @ v[index_global_V].flat + + elif isinstance(v, BlockVector): + + offset_i = ind_W + for (i, Wi) in enumerate(W.spaces): + + offset_j = ind_V + for (j, Vj) in enumerate(V.spaces): + + self._dot_recursive(v[j], out[i], ind_V=offset_j, ind_W=offset_i) + + offset_j += Vj.dimension + + offset_i += Wi.dimension diff --git a/psydac/linalg/tests/test_block.py b/psydac/linalg/tests/test_block.py index ff3bf1e6c..23a53046a 100644 --- a/psydac/linalg/tests/test_block.py +++ b/psydac/linalg/tests/test_block.py @@ -10,6 +10,7 @@ from psydac.linalg.block import BlockVectorSpace, BlockVector from psydac.linalg.block import BlockLinearOperator from psydac.linalg.utilities import array_to_psydac, petsc_to_psydac +from psydac.linalg.sparse import SparseMatrixLinearOperator from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.cart import DomainDecomposition, CartDecomposition @@ -727,6 +728,87 @@ def test_block_linear_operator_serial_dot( dtype, n1, n2, p1, p2, P1, P2 ): @pytest.mark.parametrize( 'dtype', [float, complex] ) @pytest.mark.parametrize( 'n1', [8, 16] ) @pytest.mark.parametrize( 'n2', [8, 12] ) +@pytest.mark.parametrize( 'p1', [1, 3] ) +@pytest.mark.parametrize( 'p2', [1, 2] ) +@pytest.mark.parametrize( 'P1', [True, False] ) +@pytest.mark.parametrize( 'P2', [True] ) + +def test_sparse_matrix_linear_operator_serial_dot( dtype, n1, n2, p1, p2, P1, P2 ): + # set seed for reproducibility + seed(n1*n2*p1*p2) + + D = DomainDecomposition([n1,n2], periods=[P1,P2]) + + # Partition the points + npts = [n1,n2] + global_starts, global_ends = compute_global_starts_ends(D, npts) + + cart = CartDecomposition(D, npts, global_starts, global_ends, pads=[p1,p2], shifts=[1,1]) + + # Create vector spaces, stencil matrices, and stencil vectors + V = StencilVectorSpace( cart, dtype=dtype ) + M1 = StencilMatrix( V, V) + M2 = StencilMatrix( V, V ) + M3 = StencilMatrix( V, V ) + x1 = StencilVector( V ) + x2 = StencilVector( V ) + + # Fill in stencil matrices based on diagonal index + if dtype==complex: + f=lambda k1,k2: 10j*k1+k2 + else: + f=lambda k1,k2: 10*k1+k2 + + for k1 in range(-p1,p1+1): + for k2 in range(-p2,p2+1): + M1[:,:,k1,k2] = f(k1,k2) + M2[:,:,k1,k2] = f(k1,k2)+2. + M3[:,:,k1,k2] = f(k1,k2)+5. + + M1.remove_spurious_entries() + M2.remove_spurious_entries() + M3.remove_spurious_entries() + + # Fill in vector with random values, then update ghost regions + for i1 in range(n1): + for i2 in range(n2): + x1[i1,i2] = 2.0*random() - 1.0 + x2[i1,i2] = 5.0*random() - 1.0 + x1.update_ghost_regions() + x2.update_ghost_regions() + + W = BlockVectorSpace(V, V) + + # Construct a BlockLinearOperator object containing M1, M2, M, using 3 ways + # |M1 M2| + # L = | | + # |M3 0 | + + dict_blocks = {(0,0):M1, (0,1):M2, (1,0):M3} + + L = BlockLinearOperator( W, W, blocks=dict_blocks ) + Lm = SparseMatrixLinearOperator(W, W, L.tosparse().tocsr()) + + # Construct a BlockVector object containing x1 and x2 + # |x1| + # X = | | + # |x2| + + X = BlockVector(W) + X[0] = x1 + X[1] = x2 + + # Compute BlockLinearOperator product + Y = L.dot(X) + + Ym = Lm.dot(X) + + # Check data in 1D array + assert np.allclose( Ym.toarray(), Y.toarray(), rtol=1e-12, atol=1e-12 ) +#=============================================================================== +@pytest.mark.parametrize( 'dtype', [float, complex] ) +@pytest.mark.parametrize( 'n1', [8, 16] ) +@pytest.mark.parametrize( 'n2', [8, 12] ) @pytest.mark.parametrize( 'p1', [1, 2] ) @pytest.mark.parametrize( 'p2', [1, 3] ) @pytest.mark.parametrize( 'P1', [True, False] ) diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index 57a9a6b86..fb13a2e6d 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -4,14 +4,14 @@ from math import sqrt from psydac.linalg.basic import Vector -from psydac.linalg.stencil import StencilVectorSpace, StencilVector +from psydac.linalg.stencil import StencilVector, StencilVectorSpace from psydac.linalg.block import BlockVector, BlockVectorSpace from psydac.linalg.topetsc import petsc_local_to_psydac, get_npts_per_block __all__ = ( 'array_to_psydac', 'petsc_to_psydac', - '_sym_ortho' + '_sym_ortho', ) #==============================================================================