Source code for struphy.feec.mass

import inspect
import logging
from copy import deepcopy
from typing import Callable

import cunumpy as xp
from feectools.api.settings import PSYDAC_BACKEND_GPYCCEL
from feectools.ddm.mpi import MockComm
from feectools.ddm.mpi import mpi as MPI
from feectools.fem.tensor import FemSpace, TensorFemSpace
from feectools.fem.vector import VectorFemSpace
from feectools.linalg.basic import IdentityOperator, InverseLinearOperator, LinearOperator, Vector
from feectools.linalg.block import BlockLinearOperator, BlockVector
from feectools.linalg.solvers import inverse
from feectools.linalg.stencil import StencilDiagonalMatrix, StencilMatrix, StencilVector

from struphy import equils
from struphy.feec import mass_kernels
from struphy.feec.linear_operators import BoundaryOperator, LinOpWithTransp
from struphy.feec.psydac_derham import Derham, SplineFunction
from struphy.feec.utilities import LocalProjectionMatrix, LocalRotationMatrix, get_quad_grids
from struphy.fields_background.base import MHDequilibrium
from struphy.geometry.base import Domain
from struphy.io.options import LiteralOptions
from struphy.linear_algebra.solver import SolverParameters
from struphy.polar.basic import PolarVector
from struphy.polar.linear_operators import PolarExtractionOperator
from struphy.utils.docstring_converter import auto_convert_docstring, info
from struphy.utils.pyccel import Pyccelkernel
from struphy.utils.utils import __class_with_params_repr_no_defaults__

logger = logging.getLogger("struphy")


[docs] class WeightedMassOperators: r""" Collection of pre-defined :class:`struphy.feec.mass.WeightedMassOperator`. Parameters ---------- derham : Derham Discrete de Rham sequence on the logical unit cube. domain : :ref:`avail_mappings` Mapping from logical unit cube to physical domain and corresponding metric coefficients. eq_mhd : MHDequilibrium | None MHD equilibrium object. matrix_free : bool If set to true will not compute the matrix associated with the operator but directly compute the product when called. """ def __init__( self, derham: Derham, domain: Domain, eq_mhd: MHDequilibrium | None = None, matrix_free: bool = False, ): self._derham = derham self._domain = domain self._matrix_free = matrix_free self._eq_mhd = eq_mhd if self._eq_mhd is None: self._eq_mhd = equils.HomogenSlab() if not hasattr(self.eq_mhd, "_domain"): self._eq_mhd.domain = self._domain # only for M1 Mac users PSYDAC_BACKEND_GPYCCEL["flags"] = "-O3 -march=native -mtune=native -ffast-math -ffree-line-length-none" @property def derham(self) -> Derham: """Discrete de Rham sequence on the logical unit cube.""" return self._derham @property def domain(self) -> Domain: """Mapping from the logical unit cube to the physical domain with corresponding metric coefficients.""" return self._domain @property def eq_mhd(self) -> MHDequilibrium | None: """MHD equilibrium object.""" return self._eq_mhd @property def matrix_free(self) -> bool: """If set to true will not compute the matrix associated with the operators but directly compute the dot product when called.""" return self._matrix_free def info(self): print("The mass matrices of the Derham complex are:") self.M0.info() self.M1.info() self.M2.info() self.M3.info() print("Available mass operators as properties:") li = "" for name, method in inspect.getmembers(self.__class__, predicate=inspect.isdatadescriptor): if name.startswith("M") and isinstance(getattr(self.__class__, name), property): li += f"{name}, " li = li.rstrip(", ") print(f"{li}") print("\nTo see more details on a mass operator, call the info() method on it, e.g. mass_ops.M0.info().") ####################################################################### # Mass matrices related to L2-scalar products in all 3d derham spaces # ####################################################################### @auto_convert_docstring @property def M0(self): r""" Standard mass matrix for 0-forms (H1 space): .. math:: \mathbb M^0_{ijk, mno} = \int \Lambda^0_{ijk} \Lambda^0_{mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M0"): self._M0 = self.create_weighted_mass( "H1", "H1", weights=("sqrt_g",), name="M0", assemble=True, ) return self._M0 @auto_convert_docstring @property def M1(self): r""" Standard mass matrix for 1-forms (Hcurl space) as 3x3 block matrix indexed by :math:`(\mu, \nu)`: .. math:: \mathbb M^1_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^1_{\mu,ijk}\, G^{-1} \vec{\Lambda}^1_{\nu, mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M1"): self._M1 = self.create_weighted_mass( "Hcurl", "Hcurl", weights=("Ginv", "sqrt_g"), name="M1", assemble=True, ) return self._M1 @auto_convert_docstring @property def M2(self): r""" Standard mass matrix for 2-forms (Hdiv space) as 3x3 block matrix indexed by :math:`(\mu, \nu)`: .. math:: \mathbb M^2_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^2_{\mu,ijk} G \vec{\Lambda}^2_{\nu, mno} \frac{1}{\sqrt{g}} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M2"): self._M2 = self.create_weighted_mass( "Hdiv", "Hdiv", weights=("G", "1/sqrt_g"), name="M2", assemble=True, ) return self._M2 @auto_convert_docstring @property def M3(self): r""" Standard mass matrix for 3-forms (L2 space): .. math:: \mathbb M^3_{ijk, mno} = \int \Lambda^3_{ijk}\, \Lambda^3_{mno} \frac{1}{\sqrt{g}}\, \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M3"): self._M3 = self.create_weighted_mass( "L2", "L2", weights=("1/sqrt_g",), name="M3", assemble=True, ) return self._M3 @auto_convert_docstring @property def Mv(self): r""" Standard mass matrix for vector 0-forms (H1vec space) as 3x3 block matrix indexed by :math:`(\mu, \nu)`: .. math:: \mathbb M^v_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^v_{\mu,ijk} G \vec{\Lambda}^v_{\nu, mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_Mv"): self._Mv = self.create_weighted_mass( "H1vec", "H1vec", weights=("G", "sqrt_g"), name="Mv", assemble=True, ) return self._Mv ###################################### # Predefined weighted mass operators # ###################################### @auto_convert_docstring @property def M2stab_for_rot(self): r""" Stabilization matrix for the rot-problem u2 + B2 x u2 = G*f2 as 3x3 block matrix indexed by :math:`(\mu, \nu)`: .. math:: \mathbb M^2_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^2_{\mu,ijk} \vec{\Lambda}^2_{\nu, mno} \frac{1}{\sqrt{g}} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M2stab_for_rot"): self._M2stab_for_rot = self.create_weighted_mass( "Hdiv", "Hdiv", weights=("Identity", "1/sqrt_g"), name="M2stab_for_rot", assemble=True, ) return self._M2stab_for_rot @auto_convert_docstring @property def M1n(self): r""" Mass matrix .. math:: \mathbb M^{1,n}_{(\mu,ijk), (\nu,mno)} = \int n^0_{\textnormal{eq}}(\boldsymbol{\eta}) \vec{\Lambda}^1_{\mu,ijk} G^{-1} \vec{\Lambda}^1_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. where :math:`n^0_{\textnormal{eq}}(\boldsymbol{\eta})` is an MHD equilibrium density (0-form). """ if not hasattr(self, "_M1n"): self._M1n = self.create_weighted_mass( "Hcurl", "Hcurl", weights=( "Ginv", "sqrt_g", lambda *etas: self.eq_mhd.n0(*etas), ), name="M1n", assemble=True, ) return self._M1n @auto_convert_docstring @property def M2n(self): r""" Mass matrix .. math:: \mathbb M^{2,n}_{(\mu,ijk), (\nu,mno)} = \int n^0_{\textnormal{eq}}(\boldsymbol{\eta}) \vec{\Lambda}^2_{\mu,ijk} G \vec{\Lambda}^2_{\nu,mno} \frac{1}{\sqrt{g}} \textnormal{d}\boldsymbol{\eta}. where :math:`n^0_{\textnormal{eq}}(\boldsymbol{\eta})` is an MHD equilibrium density (0-form). """ if not hasattr(self, "_M2n"): self._M2n = self.create_weighted_mass( "Hdiv", "Hdiv", weights=( "G", "1/sqrt_g", lambda *etas: self.eq_mhd.n0(*etas), ), name="M2n", assemble=True, ) return self._M2n @auto_convert_docstring @property def Mvn(self): r""" Mass matrix .. math:: \mathbb M^{v,n}_{(\mu,ijk), (\nu,mno)} = \int n^0_{\textnormal{eq}}(\boldsymbol{\eta}) \vec{\Lambda}^v_{\mu,ijk} G \vec{\Lambda}^v_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. where :math:`n^0_{\textnormal{eq}}(\boldsymbol{\eta})` is an MHD equilibrium density (0-form). """ if not hasattr(self, "_Mvn"): self._Mvn = self.create_weighted_mass( "H1vec", "H1vec", weights=( "G", "sqrt_g", lambda *etas: self.eq_mhd.n0(*etas), ), name="Mvn", assemble=True, ) return self._Mvn @auto_convert_docstring @property def M1ninv(self): r""" Mass matrix .. math:: \mathbb M^{1,\frac{1}{n}}_{(\mu,ijk), (\nu,mno)} = \int \frac{1}{n^0_{\textnormal{eq}}(\boldsymbol{\eta})} \vec{\Lambda}^1_{\mu,ijk} G^{-1} \vec{\Lambda}^1_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. where :math:`n^0_{\textnormal{eq}}(\boldsymbol{\eta})` is an MHD equilibrium density (0-form). """ if not hasattr(self, "_M1ninv"): self._M1ninv = self.create_weighted_mass( "Hcurl", "Hcurl", weights=( "Ginv", "sqrt_g", lambda *etas: 1 / self.eq_mhd.n0(*etas), ), name="M1ninv", assemble=True, ) return self._M1ninv @auto_convert_docstring @property def M1J(self): r""" Mass matrix .. math:: \mathbb M^{1,J}_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^1_{\mu,ijk} G^{-1} \mathcal{R}(J) \vec{\Lambda}^2_{\nu,mno} \textnormal{d}\boldsymbol{\eta}. with the rotation matrix .. math:: \mathcal{R}(J)_{\alpha,\nu} := \epsilon_{\alpha\beta\nu} J^2_{\textnormal{eq},\beta},\qquad s.t. \qquad \mathcal{R}(J) \vec{v} = \vec{J}^2_{\textnormal{eq}} \times \vec{v}, where :math:`\epsilon_{\alpha \beta \nu}` stands for the Levi-Civita tensor and :math:`J^2_{\textnormal{eq}, \beta}` is the :math:`\beta`-component of the MHD equilibrium current density (2-form). """ if not hasattr(self, "_M1J"): assert self.eq_mhd is not None, ( "M1J requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) rot_J = LocalRotationMatrix( self.eq_mhd.j2_1, self.eq_mhd.j2_2, self.eq_mhd.j2_3, ) self._M1J = self.create_weighted_mass( "Hdiv", "Hcurl", weights=("Ginv", rot_J), name="M1J", assemble=True, ) return self._M1J @auto_convert_docstring @property def M2J(self): r""" Mass matrix .. math:: \mathbb M^{2,J}_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^2_{\mu,ijk} \mathcal{R}(J) \vec{\Lambda}^2_{\nu,mno} \frac{1}{\sqrt{g}} \textnormal{d}\boldsymbol{\eta}. with the rotation matrix .. math:: \mathcal{R}(J)_{\alpha,\nu} := \epsilon_{\alpha\beta\nu} J^2_{\textnormal{eq},\beta},\qquad s.t. \qquad \mathcal{R}(J) \vec{v} = \vec{J}^2_{\textnormal{eq}} \times \vec{v}, where :math:`\epsilon_{\alpha \beta \nu}` stands for the Levi-Civita tensor and :math:`J^2_{\textnormal{eq}, \beta}` is the :math:`\beta`-component of the MHD equilibrium current density (2-form). """ if not hasattr(self, "_M2J"): assert self.eq_mhd is not None, ( "M2J requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) rot_J = LocalRotationMatrix( self.eq_mhd.j2_1, self.eq_mhd.j2_2, self.eq_mhd.j2_3, ) self._M2J = self.create_weighted_mass( "Hdiv", "Hdiv", weights=(rot_J, "1/sqrt_g"), name="M2J", assemble=True, ) return self._M2J @auto_convert_docstring @property def MvJ(self): r""" Mass matrix .. math:: \mathbb M^{v,J}_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^v_{\mu,ijk} \mathcal{R}(J) \vec{\Lambda}^v_{\nu,mno} \textnormal{d}\boldsymbol{\eta}. with the rotation matrix .. math:: \mathcal{R}(J)_{\alpha,\nu} := \epsilon_{\alpha\beta\nu} J^2_{\textnormal{eq},\beta},\qquad s.t. \qquad \mathcal{R}(J) \vec{v} = \vec{J}^2_{\textnormal{eq}} \times \vec{v}, where :math:`\epsilon_{\alpha \beta \nu}` stands for the Levi-Civita tensor and :math:`J^2_{\textnormal{eq}, \beta}` is the :math:`\beta`-component of the MHD equilibrium current density (2-form). """ if not hasattr(self, "_MvJ"): assert self.eq_mhd is not None, ( "MvJ requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) rot_J = LocalRotationMatrix( self.eq_mhd.j2_1, self.eq_mhd.j2_2, self.eq_mhd.j2_3, ) self._MvJ = self.create_weighted_mass( "Hdiv", "H1vec", weights=(rot_J,), name="MvJ", assemble=True, ) return self._MvJ @auto_convert_docstring @property def M2B_div0(self): r""" Mass matrix .. math:: \mathbb M^{2,B}_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^2_{\mu,ijk} \mathcal{R}(B) \vec{\Lambda}^2_{\nu,mno} \frac{1}{\sqrt{g}} \textnormal{d}\boldsymbol{\eta}. with the rotation matrix .. math:: \mathcal{R}(B)_{\alpha,\nu} := \epsilon_{\alpha\beta\nu} B^2_{\textnormal{eq},\beta},\qquad s.t. \qquad \mathcal{R}(B) \vec{v} = \vec{B}^2_{\textnormal{eq}} \times \vec{v}, where :math:`\epsilon_{\alpha \beta \nu}` stands for the Levi-Civita tensor and :math:`B^2_{\textnormal{eq}, \beta}` is the :math:`\beta`-component of the MHD equilibrium magnetic field (2-form). """ if not hasattr(self, "_M2B_div0"): assert self.eq_mhd is not None, ( "M2B_div0 requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) a_eq = self.derham.P1( [ self.eq_mhd.a1_1, self.eq_mhd.a1_2, self.eq_mhd.a1_3, ], ) tmp_b2 = self.derham.curl.dot(a_eq) b02fun = self.derham.create_spline_function("b02", "Hdiv") b02fun.vector = tmp_b2 def b02funx(x, y, z): return b02fun( x, y, z, local=True, )[0] def b02funy(x, y, z): return b02fun( x, y, z, local=True, )[1] def b02funz(x, y, z): return b02fun( x, y, z, local=True, )[2] rot_B = LocalRotationMatrix( b02funx, b02funy, b02funz, ) self._M2B_div0 = self.create_weighted_mass( "Hdiv", "Hdiv", weights=(rot_B, "1/sqrt_g"), name="M2B_div0", assemble=True, ) return self._M2B_div0 @auto_convert_docstring @property def M2B(self): r""" Mass matrix .. math:: \mathbb M^{2,B}_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^2_{\mu,ijk} \mathcal{R}(B) \vec{\Lambda}^2_{\nu,mno} \frac{1}{\sqrt{g}} \textnormal{d}\boldsymbol{\eta}. with the rotation matrix .. math:: \mathcal{R}(B)_{\alpha,\nu} := \epsilon_{\alpha\beta\nu} B^2_{\textnormal{eq},\beta},\qquad s.t. \qquad \mathcal{R}(B) \vec{v} = \vec{B}^2_{\textnormal{eq}} \times \vec{v}, where :math:`\epsilon_{\alpha \beta \nu}` stands for the Levi-Civita tensor and :math:`B^2_{\textnormal{eq}, \beta}` is the :math:`\beta`-component of the MHD equilibrium magnetic field (2-form). """ if not hasattr(self, "_M2B"): assert self.eq_mhd is not None, ( "M2B requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) rot_B = LocalRotationMatrix( self.eq_mhd.b2_1, self.eq_mhd.b2_2, self.eq_mhd.b2_3, ) self._M2B = self.create_weighted_mass( "Hdiv", "Hdiv", weights=(rot_B, "1/sqrt_g"), name="M2B", assemble=True, ) return self._M2B @auto_convert_docstring @property def M2Bn(self): r""" Mass matrix .. math:: \mathbb M^{2,BN}_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^2_{\mu,ijk} \mathcal{R}(B) \vec{\Lambda}^2_{\nu,mno} \frac{1}{n^0_{\textnormal{eq}}(\boldsymbol{\eta})} \frac{1}{\sqrt{g}} \textnormal{d}\boldsymbol{\eta}. with the rotation matrix .. math:: \mathcal{R}(B)_{\alpha,\nu} := \epsilon_{\alpha\beta\nu} B^2_{\textnormal{eq},\beta},\qquad s.t. \qquad \mathcal{R}(B) \vec{v} = \vec{B}^2_{\textnormal{eq}} \times \vec{v}, where :math:`\epsilon_{\alpha \beta \nu}` stands for the Levi-Civita tensor and :math:`B^2_{\textnormal{eq}, \beta}` is the :math:`\beta`-component of the MHD equilibrium magnetic field (2-form). """ if not hasattr(self, "_M2Bn"): assert self.eq_mhd is not None, ( "M2Bn requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) a_eq = self.derham.P1( [ self.eq_mhd.a1_1, self.eq_mhd.a1_2, self.eq_mhd.a1_3, ], ) tmp_b2 = self.derham.curl.dot(a_eq) b02fun = self.derham.create_spline_function("b02", "Hdiv") b02fun.vector = tmp_b2 def b02funx(x, y, z): return b02fun( x, y, z, local=True, )[0] def b02funy(x, y, z): return b02fun( x, y, z, local=True, )[1] def b02funz(x, y, z): return b02fun( x, y, z, local=True, )[2] rot_B = LocalRotationMatrix( b02funx, b02funy, b02funz, ) self._M2Bn = self.create_weighted_mass( "Hdiv", "Hdiv", weights=( rot_B, "1/sqrt_g", lambda *etas: 1 / self.eq_mhd.n0(*etas), ), name="M2Bn", assemble=True, ) return self._M2Bn @auto_convert_docstring @property def M1Bninv(self): r""" Mass matrix .. math:: \mathbb M^{1,B\frac{1}{n}}_{(\mu,ijk), (\nu,mno)} = \int \frac{1}{n^0_{\textnormal{eq}}(\boldsymbol{\eta})} \vec{\Lambda}^1_{\mu,ijk} G^{-1} \mathcal{R}(B)_{\alpha,\gamma} G^{-1}_{\gamma,\nu} \vec{\Lambda}^1_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. with the rotation matrix .. math:: \mathcal{R}(B)_{\alpha,\nu} := \epsilon_{\alpha\beta\nu} B^2_{\textnormal{eq},\beta},\qquad s.t. \qquad \mathcal{R}(B) \vec{v} = \vec{B}^2_{\textnormal{eq}} \times \vec{v}, where :math:`\epsilon_{\alpha \beta \nu}` stands for the Levi-Civita tensor and :math:`B^2_{\textnormal{eq}, \beta}` is the :math:`\beta`-component of the MHD equilibrium magnetic field (2-form). """ if not hasattr(self, "_M1Bninv"): assert self.eq_mhd is not None, ( "M1Bninv requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) rot_B = LocalRotationMatrix( self.eq_mhd.b2_1, self.eq_mhd.b2_2, self.eq_mhd.b2_3, ) self._M1Bninv = self.create_weighted_mass( "Hcurl", "Hcurl", weights=( "Ginv", rot_B, "Ginv", "sqrt_g", lambda *etas: 1 / self.eq_mhd.n0(*etas), ), name="M1Bninv", assemble=True, ) return self._M1Bninv @auto_convert_docstring @property def M1para(self): r""" Mass matrix .. math:: \mathbb M^{1,\parallel}_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^1_{\mu,ijk} b_0 b_0^\top \vec{\Lambda}^1_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M1para"): bb = LocalProjectionMatrix(self.eq_mhd.unit_bv_1, self.eq_mhd.unit_bv_2, self.eq_mhd.unit_bv_3) self._M1para = self.create_weighted_mass( "Hcurl", "Hcurl", weights=( bb, "sqrt_g", ), name="M1para", assemble=True, ) return self._M1para @auto_convert_docstring @property def M1perp(self): r""" Mass matrix .. math:: \mathbb M^{1,\perp}_{(\mu,ijk), (\nu,mno)} = \int \vec{\Lambda}^1_{\mu,ijk} \left(G^{-1} - b_0 b_0^\top \right) \vec{\Lambda}^1_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M1perp"): self._M1perp = self.M1 - self.M1para return self._M1perp @auto_convert_docstring @property def M1para_MHDeq(self): r""" Mass matrix .. math:: \mathbb M^{1,\parallel}_{(\mu,ijk), (\nu,mno)} = \int \frac{n^0_{\textnormal{eq}}(\boldsymbol{\eta})}{\|B_0(\boldsymbol{\eta})\|^2} \vec{\Lambda}^1_{\mu,ijk} b_0 b_0^\top \vec{\Lambda}^1_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M1para_MHDeq"): bb = LocalProjectionMatrix(self.eq_mhd.unit_bv_1, self.eq_mhd.unit_bv_2, self.eq_mhd.unit_bv_3) self._M1para_MHDeq = self.create_weighted_mass( "Hcurl", "Hcurl", weights=( bb, lambda *etas: self.eq_mhd.n0(*etas) / self.eq_mhd.absB0(*etas) ** 2, "sqrt_g", ), name="M1para_MHDeq", assemble=True, ) return self._M1para_MHDeq @auto_convert_docstring @property def M1_MHDeq(self): r""" Mass matrix .. math:: \mathbb M^{1}_{(\mu,ijk), (\nu,mno)} = \int \frac{n^0_{\textnormal{eq}}(\boldsymbol{\eta})}{\|B_0(\boldsymbol{\eta})\|^2} \vec{\Lambda}^1_{\mu,ijk} G^{-1} \vec{\Lambda}^1_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M1_MHDeq"): self._M1_MHDeq = self.create_weighted_mass( "Hcurl", "Hcurl", weights=( "Ginv", lambda *etas: self.eq_mhd.n0(*etas) / self.eq_mhd.absB0(*etas) ** 2, "sqrt_g", ), name="M1_MHDeq", assemble=True, ) return self._M1_MHDeq @auto_convert_docstring @property def M1gyro(self): r""" Mass matrix .. math:: \mathbb M^{1,\perp}_{(\mu,ijk), (\nu,mno)} = \int \frac{n^0_{\textnormal{eq}}(\boldsymbol{\eta})}{\|B_0(\boldsymbol{\eta})\|^2} \vec{\Lambda}^1_{\mu,ijk} \left(G^{-1} - b_0 b_0^\top \right) \vec{\Lambda}^1_{\nu,mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. """ if not hasattr(self, "_M1gyro"): self._M1gyro = self.M1_MHDeq - self.M1para_MHDeq return self._M1gyro @auto_convert_docstring @property def M0ad(self): r""" Mass matrix .. math:: \mathbb M^0_{ijk, mno} = \int n^0_{\textnormal{eq}}(\boldsymbol{\eta}) \Lambda^0_{ijk} \Lambda^0_{mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. where :math:`n^0_{\textnormal{eq}}(\boldsymbol{\eta})` is an MHD equilibrium density (0-form). """ if not hasattr(self, "_M0ad"): self._M0ad = self.create_weighted_mass( "H1", "H1", weights=( lambda *etas: self.eq_mhd.n0(*etas), "sqrt_g", ), name="M0ad", assemble=True, ) return self._M0ad @auto_convert_docstring @property def M0ad_withT(self): r""" Mass matrix .. math:: \mathbb M^0_{ijk, mno} = \int \frac{n^0_{\textnormal{eq}}(\boldsymbol{\eta})}{T^0_{\textnormal{eq}}(\boldsymbol{\eta})} \Lambda^0_{ijk} \Lambda^0_{mno} \sqrt{g} \textnormal{d}\boldsymbol{\eta}. where :math:`n^0_{\textnormal{eq}}(\boldsymbol{\eta})` and :math:`T^0_{\textnormal{eq}}(\boldsymbol{\eta})` are MHD equilibrium density and electron temperature (0-forms), respectively. """ if not hasattr(self, "_M0ad_withT"): self._M0ad_withT = self.create_weighted_mass( "H1", "H1", weights=( lambda *etas: self.eq_mhd.n0(*etas) / self.eq_mhd.t0(*etas), "sqrt_g", ), name="M0ad_withT", assemble=True, ) return self._M0ad_withT @property def WMM(self): if not hasattr(self, "_WMM"): self._WMM = self.H1vecMassMatrix_density(self.derham, self, self.domain) return self._WMM @property def WMMnew(self): if not hasattr(self, "_WMMnew"): spline = self.derham.create_spline_function("l2_field", "L2") self._WMMnew = self.create_weighted_mass( "H1vec", "H1vec", weights=("G", spline), name="WMMnew", assemble=False, ) return self._WMMnew ####################################### # Wrapper around WeightedMassOperator # #######################################
[docs] def create_weighted_mass( self, V_id: str, W_id: str, *, name: str = None, weights: tuple | list | str | None = None, assemble: bool = False, transposed: bool = False, ): r"""Weighted mass matrix :math:`V^\alpha_h \to V^\beta_h` with given (matrix-valued) weight function :math:`W(\boldsymbol \eta)`: .. math:: \mathbb M_{(\mu, ijk), (\nu, mno)}(W) = \int \Lambda^\beta_{\mu, ijk}\, W_{\mu,\nu}(\boldsymbol \eta)\, \Lambda^\alpha_{\nu, mno} \, \textnormal d \boldsymbol\eta. Here, :math:`\alpha \in \{0, 1, 2, 3, v\}` indicates the domain and :math:`\beta \in \{0, 1, 2, 3, v\}` indicates the co-domain of the operator. Parameters ---------- V_id : str Specifier for the domain of the operator ('H1', 'Hcurl', 'Hdiv', 'L2' or 'H1vec'). W_id : str Specifier for the co-domain of the operator ('H1', 'Hcurl', 'Hdiv', 'L2' or 'H1vec'). name: str Name of the operator. weights : None | str | tuple | list Information about the weights/block structure of the operator. Four cases are possible: 1. ``None`` : all blocks are allocated, disregarding zero-blocks or any symmetry. 2. ``str`` : for square block matrices (V=W), a symmetry can be set in order to accelerate the assembly process. Possible strings are ``symm`` (symmetric), ``asym`` (anti-symmetric) and ``diag`` (diagonal). 3. ``1D tuple`` : most common and recommended input format. Entries are processed from left to right and multiplied together. Supported tuple entries are: - Strings (predefined names) for metric and Jacobian-related weights: ``'G'``, ``'Ginv'``, ``'DFinv'``, ``'DFinvT'``, ``'sqrt_g'``, ``'1/sqrt_g'``, ``'Identity'``. - Callables (including objects such as local rotation matrices) returning either scalar values or ``3x3`` matrix values at quadrature points. - Nested ``3x3`` Python lists (constant matrix entries). - :class:`~struphy.feec.psydac_derham.SplineFunction` instances. Example: ``weights=('Ginv', 'sqrt_g')`` 4. ``2D list`` : 2d list with the same number of rows/columns as the number of components of the domain/codomain spaces. The entries can be either a) callables or b) xp.ndarrays representing the weights at the quadrature points. If an entry is zero or ``None``, the corresponding block is set to ``None`` to accelerate the dot product. assemble: bool Whether to assemble the weighted mass matrix, i.e. computes the integrals with :class:`~struphy.feec.mass.WeightedMassOperators.assemble` transposed: bool Whether to assemble the transposed operator. Returns ------- out : A WeightedMassOperator object. """ logger.debug(f"\nCreating weighted mass matrix {name} from {V_id} to {W_id}.") spline_functions = {} if isinstance(weights, tuple): # Case 3 (1D tuple) # save callables in lists for later evaluation at quadrature points f_call_scalars = [] f_call_column_vector = None f_call_row_vector = None f_call_matrices = [] for n, f in enumerate(weights): logger.debug(f"Processing weight #{n}") if isinstance(f, str): # determine the callable and add to list f_call_ logger.debug(f"Processing string weight {f}.") if f == "G": f_call = lambda e1, e2, e3: self.domain.metric(e1, e2, e3, change_out_order=True) f_call_matrices.append(f_call) elif f == "Ginv": f_call = lambda e1, e2, e3: self.domain.metric_inv(e1, e2, e3, change_out_order=True) f_call_matrices.append(f_call) elif f == "DFinv": f_call = lambda e1, e2, e3: self.domain.jacobian_inv(e1, e2, e3, change_out_order=True) f_call_matrices.append(f_call) elif f == "DFinvT": f_call = lambda e1, e2, e3: self.domain.jacobian_inv( e1, e2, e3, change_out_order=True, transposed=True ) f_call_matrices.append(f_call) elif f == "sqrt_g": f_call = lambda e1, e2, e3: abs(self.domain.jacobian_det(e1, e2, e3)) f_call_scalars.append(f_call) elif f == "1/sqrt_g": f_call = lambda e1, e2, e3: 1.0 / abs(self.domain.jacobian_det(e1, e2, e3)) f_call_scalars.append(f_call) elif f == "Identity": def f_call(e1, e2, e3): """Identity callable.""" # to keep C-ordering the (3, 3)-part is in the last indices out = xp.zeros((3, 3, e1.shape[0], e2.shape[1], e3.shape[2]), dtype=float) out[0, 0] = 1.0 out[1, 1] = 1.0 out[2, 2] = 1.0 return xp.transpose(out, axes=(2, 3, 4, 0, 1)) f_call_matrices.append(f_call) else: raise NotImplementedError( f"The option {f} is not available.", ) elif isinstance(f, list): assert len(f) == 3 logger.debug(f"Processing nested list weight {f}.") for fi in f: assert isinstance(fi, list) assert len(fi) == 3 # copy the values of the nested list to avoid any issues with references values = tuple([tuple([value for value in fi_row]) for fi_row in f]) def f_call(e1, e2, e3): """Nested list callable.""" out = xp.zeros((3, 3, e1.shape[0], e2.shape[1], e3.shape[2]), dtype=float) for m in range(3): for n in range(3): logger.debug(f"{values[m][n] = }") out[m, n] = values[m][n] return xp.transpose(out, axes=(2, 3, 4, 0, 1)) f_call_matrices.append(f_call) elif isinstance(f, SplineFunction): logger.debug(f"Processing SplineFunction weight {f}.") spline_functions[f.name] = f continue else: # Input is a a matrix or a Rotation matrix etc. logger.debug(f"Processing callable weight {f}.") assert callable(f) # determine the output dimension of the callable and add to list f_call_ xx, yy, zz = xp.meshgrid( xp.linspace(0, 1, 1), xp.linspace(0, 1, 2), xp.linspace(0, 1, 3), indexing="ij", ) out_dim = f(xx, yy, zz).ndim if out_dim == 3: f_call_scalars.append(f) elif out_dim == 4: if V_id in ("H1", "L2") and W_id in ("Hcurl", "Hdiv", "H1vec"): f_call_column_vector = f elif V_id in ("Hcurl", "Hdiv", "H1vec") and W_id in ("H1", "L2"): f_call_row_vector = f else: raise ValueError( f"Vector weight {f} is only supported for scalar<->vector maps; got {V_id}->{W_id}." ) elif out_dim == 5: f_call_matrices.append(f) else: raise ValueError( f"Callable {f} has wrong output dimension {out_dim}.", ) # check that the dimensions of the callables are compatible with the domain and codomain spaces if f_call_column_vector is not None: assert V_id in ("H1", "L2") assert W_id in ("Hcurl", "Hdiv", "H1vec") assert len(f_call_matrices) == 0 assert f_call_row_vector is None if f_call_row_vector is not None: assert V_id in ("Hcurl", "Hdiv", "H1vec") assert W_id in ("H1", "L2") assert len(f_call_matrices) == 0 assert f_call_column_vector is None if len(f_call_matrices) > 0: assert V_id in ("Hcurl", "Hdiv", "H1vec") assert W_id in ("Hcurl", "Hdiv", "H1vec") assert f_call_column_vector is None assert f_call_row_vector is None # matrix-matrix multiplication of the callables in f_call_matrices to get a single callable if len(f_call_matrices) > 0: def f_call_matrix(e1, e2, e3): """Matrix-matrix multiplication of the callables in f_call_matrices.""" out = f_call_matrices[0](e1, e2, e3) if len(f_call_matrices) > 1: for f in f_call_matrices[1:]: # out = xp.einsum("...ij,...jk->...ik", out, f(e1, e2, e3)) out[:] = out @ f(e1, e2, e3) # the 3x3 part must be in the last two indices return out # get the evaluation points for the quadrature grid of the codomain space W_id assert W_id in self.derham.spline_attributes, ( f"Spline attributes for the codomain space {W_id} not found in the Derham object !!" ) quad_grid_pts = self.derham.spline_attributes[W_id].quad_grid_pts logger.debug(f"{len(quad_grid_pts) = }") weights_values = [] integration_grids = [] # loop over components of W_id (rows, equal to the number of entries in quad_grid_pts) for component in quad_grid_pts: grids_1d = [pts.flatten() for pts in component] grid_sizes = tuple([len(grid_1d) for grid_1d in grids_1d]) logger.debug(f"Initializing {grid_sizes = }") integration_grids += [grids_1d] # loop over components of V_id (columns) if V_id in ("H1", "L2"): weights_values += [[None]] elif V_id in ("Hcurl", "Hdiv", "H1vec"): weights_values += [[None, None, None]] else: raise ValueError(f"Unknown space identifier {V_id} for the domain.") logger.debug(f"Initialized {weights_values = }") # evaluate at quadrature points, loop over rows of W_id (components of the codomain) for m, grids_1d in enumerate(integration_grids): logger.debug(f"rows of {W_id}: {m}") E1, E2, E3, _ = Domain.prepare_eval_pts(*grids_1d) # matrix or vectors first if len(f_call_matrices) > 0: tmp = f_call_matrix(E1, E2, E3) logger.debug(f"Evaluated matrix callable with shape {tmp.shape = }") logger.debug(f"max value: {xp.max(tmp)}, min value: {xp.min(tmp)}") for n in range(len(weights_values[m])): logger.debug(f"columns of {V_id}: {n}") weights_values[m][n] = tmp[:, :, :, m, n] elif f_call_column_vector is not None: tmp = f_call_column_vector(E1, E2, E3) logger.debug(f"Evaluated column vector callable with shape {tmp.shape = }") logger.debug(f"max value: {xp.max(tmp)}, min value: {xp.min(tmp)}") for n in range(len(weights_values[m])): logger.debug(f"columns of {V_id}: {n}") weights_values[m][n] = tmp[:, :, :, m] elif f_call_row_vector is not None: tmp = f_call_row_vector(E1, E2, E3) logger.debug(f"Evaluated row vector callable with shape {tmp.shape = }") logger.debug(f"max value: {xp.max(tmp)}, min value: {xp.min(tmp)}") for n in range(len(weights_values[m])): logger.debug(f"columns of {V_id}: {n}") weights_values[m][n] = tmp[:, :, :, n] # then loop over scalars and multiply with the previous result for f_call in f_call_scalars: tmp = f_call(E1, E2, E3) logger.debug(f"Evaluated scalar callable with shape {tmp.shape = }") logger.debug(f"max value: {xp.max(tmp)}, min value: {xp.min(tmp)}") for n in range(len(weights_values[m])): logger.debug(f"columns of {V_id}: {n}") if weights_values[m][n] is None: if m == n: weights_values[m][n] = tmp else: continue else: weights_values[m][n] *= tmp else: logger.debug(f"Processing weights of type {type(weights)}.") weights_values = weights out = WeightedMassOperator( self.derham, self.derham.fem_spaces[V_id], self.derham.fem_spaces[W_id], name=name, V_extraction_op=self.derham.extraction_ops[V_id], W_extraction_op=self.derham.extraction_ops[W_id], V_boundary_op=self.derham.boundary_ops[V_id], W_boundary_op=self.derham.boundary_ops[W_id], weights_info=weights_values, spline_functions=spline_functions, transposed=transposed, matrix_free=self.matrix_free, ) if assemble: out.assemble() return out
####################################### # Aux classes (to be removed in TODO) # #######################################
[docs] class H1vecMassMatrix_density: """Wrapper around a Weighted mass operator from H1vec to H1vec whose weights are given by a 3 form""" def __init__(self, derham, mass_ops, domain): self._massop = mass_ops.create_weighted_mass("H1vec", "H1vec") self.field = derham.create_spline_function("field", "L2") integration_grid = [grid_1d.flatten() for grid_1d in derham.V0splines.quad_grid_pts[0]] self.integration_grid_spans, self.integration_grid_bn, self.integration_grid_bd = ( derham.prepare_eval_tp_fixed( integration_grid, ) ) grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) self._f_values = xp.zeros(grid_shape, dtype=float) metric = domain.metric(*integration_grid) self._mass_metric_term = deepcopy(metric) self._full_term_mass = deepcopy(metric) self.domain_symbolic_name = self.massop.domain_symbolic_name @property def massop( self, ): """The WeightedMassOperator""" return self._massop @property def inv( self, ): """The inverse WeightedMassOperator""" if not hasattr(self, "_inv"): self._create_inv() return self._inv
[docs] def update_weight(self, coeffs): """Update the weighted mass matrix operator""" self.field.vector = coeffs f_values = self.field.eval_tp_fixed_loc( self.integration_grid_spans, self.integration_grid_bd, out=self._f_values, ) for i in range(3): for j in range(3): self._full_term_mass[i, j] = f_values * self._mass_metric_term[i, j] self._massop.assemble( [ [self._full_term_mass[0, 0], self._full_term_mass[0, 1], self._full_term_mass[0, 2]], [ self._full_term_mass[1, 0], self._full_term_mass[ 1, 1, ], self._full_term_mass[1, 2], ], [self._full_term_mass[2, 0], self._full_term_mass[2, 1], self._full_term_mass[2, 2]], ], ) if hasattr(self, "_inv") and self.inv._options["pc"] is not None: self.inv._options["pc"].update_mass_operator(self.massop)
def _create_inv(self, type="pcg", tol=1e-16, maxiter=500): """Inverse the weighted mass matrix, preconditioner must be set outside via self._inv._options['pc'] = ...""" self._inv = inverse( self.massop, type, pc=None, tol=tol, maxiter=maxiter, recycle=True, )
[docs] class WeightedMassOperator(LinOpWithTransp): r""" Class for assembling weighted mass matrices in 3d. Weighted mass matrices :math:`\mathbb M^{\beta\alpha}: \mathbb R^{N_\alpha} \to \mathbb R^{N_\beta}` are of the general form .. math:: \mathbb M^{\beta \alpha}_{(\mu,ijk),(\nu,mno)} = \int_{[0, 1]^3} \Lambda^\beta_{\mu,ijk} \, A_{\mu,\nu} \, \Lambda^\alpha_{\nu,mno} \, \textnormal d^3 \boldsymbol\eta\,, where the weight fuction :math:`A` is a tensor of rank 0, 1 or 2, depending on domain and co-domain of the operator, and :math:`\Lambda^\alpha_{\nu, mno}` is the B-spline basis function with tensor-product index :math:`mno` of the :math:`\nu`-th component in the space :math:`V^\alpha_h`. These matrices are sparse and stored in StencilMatrix format. Finally, :math:`\mathbb M^{\beta\alpha}` can be multiplied by :class:`~struphy.polar.linear_operators.PolarExtractionOperator` and :class:`~struphy.feec.linear_operators.BoundaryOperator`, :math:`\mathbb B\, \mathbb E\, \mathbb M^{\beta\alpha} \mathbb E^T \mathbb B^T`, to account for :ref:`polar_splines` and/or :ref:`feec_bcs`, respectively. Parameters ---------- derham : Derham Struphy Derham object. V : TensorFemSpace | VectorFemSpace Tensor product spline space from feectools.fem.tensor (domain, input space). W : TensorFemSpace | VectorFemSpace Tensor product spline space from feectools.fem.tensor (codomain, output space). name : str Name of the operator. V_extraction_op : PolarExtractionOperator, optional Extraction operator to polar sub-space of V. W_extraction_op : PolarExtractionOperator, optional Extraction operator to polar sub-space of W. V_boundary_op : BoundaryOperator, optional Boundary operator that sets essential boundary conditions. W_boundary_op : BoundaryOperator, optional Boundary operator that sets essential boundary conditions. weights_info : NoneType | str | list Information about the weights/block structure of the operator. Three cases are possible: 1. ``None`` : all blocks are allocated, disregarding zero-blocks or any symmetry. 2. ``str`` : for square block matrices (V=W), a symmetry can be set in order to accelerate the assembly process. Possible strings are ``symm`` (symmetric), ``asym`` (anti-symmetric) and ``diag`` (diagonal). 3. ``list`` : 2d list with the same number of rows/columns as the number of components of the domain/codomain spaces. The entries can be either a) callables or b) xp.ndarrays representing the weights at the quadrature points. If an entry is zero or ``None``, the corresponding block is set to ``None`` to accelerate the dot product. spline_functions : dict[str, SplineFunction] Dictionary of spline functions that are used as weights in the operator. The keys must be the names of the spline functions and the values the SplineFunction objects. transposed : bool Whether to assemble the transposed operator. matrix_free : bool If set to true will not compute the matrix associated with the operator but directly compute the product when called """ def __init__( self, derham: Derham, V: TensorFemSpace | VectorFemSpace, W: TensorFemSpace | VectorFemSpace, name: str = None, V_extraction_op: PolarExtractionOperator | IdentityOperator = None, W_extraction_op: PolarExtractionOperator | IdentityOperator = None, V_boundary_op: BoundaryOperator | IdentityOperator = None, W_boundary_op: BoundaryOperator | IdentityOperator = None, weights_info: str | list = None, spline_functions: dict[str, SplineFunction] | None = None, transposed: bool = False, matrix_free: bool = False, nquads: tuple | list = None, ): logger.debug(f"{derham = }") logger.debug(f"{V = }") logger.debug(f"{W = }") logger.debug(f"{name = }") logger.debug(f"{V_extraction_op = }") logger.debug(f"{W_extraction_op = }") logger.debug(f"{V_boundary_op = }") logger.debug(f"{W_boundary_op = }") logger.debug(f"{type(weights_info) = }") logger.debug(f"{spline_functions = }") logger.debug(f"{transposed = }") logger.debug(f"{matrix_free = }") logger.debug(f"{nquads = }") # only for M1 Mac users PSYDAC_BACKEND_GPYCCEL["flags"] = "-O3 -march=native -mtune=native -ffast-math -ffree-line-length-none" self._derham = derham self._nquads = nquads self._V = V self._W = W self._name = name # spline functions that are used as weights in the operator, to be evaluated at quadrature points self._spline_functions = spline_functions if spline_functions is not None else {} # set basis extraction operators if V_extraction_op is not None: assert V_extraction_op.domain == V.coeff_space self._V_extraction_op = V_extraction_op else: self._V_extraction_op = IdentityOperator(V.coeff_space) if W_extraction_op is not None: assert W_extraction_op.domain == W.coeff_space self._W_extraction_op = W_extraction_op else: self._W_extraction_op = IdentityOperator(W.coeff_space) # set boundary operators if V_boundary_op is not None: self._V_boundary_op = V_boundary_op else: self._V_boundary_op = IdentityOperator( self._V_extraction_op.codomain, ) if W_boundary_op is not None: self._W_boundary_op = W_boundary_op else: self._W_boundary_op = IdentityOperator( self._W_extraction_op.codomain, ) self._weights_info = weights_info self._transposed = transposed self._matrix_free = matrix_free self._dtype = V.coeff_space.dtype # set domain and codomain symbolic names logger.debug(f"{V.symbolic_space = }") logger.debug(f"{W.symbolic_space = }") V_name = V.symbolic_space W_name = W.symbolic_space assert V_name in derham.spline_attributes, ( f"Spline attributes for the domain space {V_name} not found in the Derham object !!" ) assert W_name in derham.spline_attributes, ( f"Spline attributes for the codomain space {W_name} not found in the Derham object !!" ) if transposed: self._domain_femspace = W self._domain_symbolic_name = W_name self._codomain_femspace = V self._codomain_symbolic_name = V_name else: self._domain_femspace = V self._domain_symbolic_name = V_name self._codomain_femspace = W self._codomain_symbolic_name = W_name # Are both space scalar spaces : useful to know if _dof_mat will be Stencil or Block Matrix self._is_scalar = False if isinstance(V, TensorFemSpace) and isinstance(W, TensorFemSpace): self._is_scalar = True # ====== initialize Stencil-/BlockLinearOperator ==== # collect TensorFemSpaces for each component in tuple if isinstance(V, TensorFemSpace): Vspaces = (V,) else: Vspaces = V.spaces if isinstance(W, TensorFemSpace): Wspaces = (W,) else: Wspaces = W.spaces # initialize blocks according to given symmetry and set zero default weights if isinstance(weights_info, str): self._symmetry = weights_info assert V_name == W_name, "only square matrices (V=W) allowed!" assert ( len( V_name, ) > 2 ), "only block matrices with domain/codomain spaces Hcurl, Hdiv and H1vec are allowed!" if self._matrix_free: if weights_info == "symm": blocks = [ [StencilMatrixFreeMassOperator(self.derham, Vs, Ws, nquads=self.nquads) for Vs in V.spaces] for Ws in W.spaces ] elif weights_info == "asym": blocks = [ [ StencilMatrixFreeMassOperator(self.derham, Vs, Ws, nquads=self.nquads) if i != j else None for j, Vs in enumerate(V.spaces) ] for i, Ws in enumerate(W.spaces) ] elif weights_info == "diag": blocks = [ [ StencilMatrixFreeMassOperator(self.derham, Vs, Ws, nquads=self.nquads) if i == j else None for j, Vs in enumerate(V.spaces) ] for i, Ws in enumerate(W.spaces) ] else: raise NotImplementedError( f"given symmetry {weights_info} is not implemented!", ) else: if weights_info == "symm": blocks = [ [ StencilMatrix( Vs.coeff_space, Ws.coeff_space, backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True, ) for Vs in V.spaces ] for Ws in W.spaces ] elif weights_info == "asym": blocks = [ [ StencilMatrix( Vs.coeff_space, Ws.coeff_space, backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True, ) if i != j else None for j, Vs in enumerate(V.spaces) ] for i, Ws in enumerate(W.spaces) ] elif weights_info == "diag": blocks = [ [ StencilMatrix( Vs.coeff_space, Ws.coeff_space, backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True, ) if i == j else None for j, Vs in enumerate(V.spaces) ] for i, Ws in enumerate(W.spaces) ] else: raise NotImplementedError( f"given symmetry {weights_info} is not implemented!", ) self._mat = BlockLinearOperator( V.coeff_space, W.coeff_space, blocks=blocks, ) # set zero default weights with same block structure as block matrix self._weights = [] for block_row in blocks: self._weights += [[]] for block in block_row: if block is None: self._weights[-1] += [None] else: self._weights[-1] += [lambda *etas: 0 * etas[0]] # OR initialize blocks according to given weights by identifying zero blocks else: self._symmetry = None blocks = [] self._weights = [] # loop over codomain spaces (rows) for a, wspace in enumerate(Wspaces): blocks += [[]] self._weights += [[]] # quadrature points pts = [points.flatten() for points in derham.spline_attributes[W_name].quad_grid_pts[a]] # spline functions as weights: prepare for assembly on quad grid grid_shape = tuple([len(pt) for pt in pts]) self.spline_values = {} self.spans = {} self.bases = {} for name, spline in self.spline_functions.items(): assert isinstance(spline, SplineFunction), ( f"The entry {name} in spline_functions must be a SplineFunction object." ) self.spline_values[name] = xp.zeros(grid_shape, dtype=float) self.spans[name], bns, bds = derham.prepare_eval_tp_fixed(pts) if spline.space_id == "H1": self.bases[name] = bns elif spline.space_id == "L2": self.bases[name] = bds else: raise NotImplementedError( f"Spline functions in spline_functions must be defined on H1 or L2 spaces, but {spline.space_id} was given for the spline function {name}.", ) # loop over domain spaces (columns) for b, vspace in enumerate(Vspaces): # set zero default weights if weights is None if weights_info is None: if self._matrix_free: blocks[-1] += [ StencilMatrixFreeMassOperator(self.derham, vspace, wspace, self.nquads), ] else: blocks[-1] += [ StencilMatrix( vspace.coeff_space, wspace.coeff_space, backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True, ), ] self._weights[-1] += [lambda *etas: 0 * etas[0]] else: # A block can be locally zero on this MPI rank but non-zero on another rank. # We therefore check whether the block is globally non-zero before deciding # whether to allocate the corresponding StencilMatrix. All ranks must make # the same block-allocation decision, otherwise exchange_assembly_data() # will communicate incompatible block structures. loc_weight = weights_info[a][b] if loc_weight is None: mat_w = None local_nonzero = xp.array(False, dtype=bool) else: if callable(loc_weight): PTS = xp.meshgrid(*pts, indexing="ij") mat_w = loc_weight(*PTS).copy() elif isinstance(loc_weight, xp.ndarray): mat_w = loc_weight else: raise TypeError(f"Invalid weight type: {type(loc_weight)}") logger.debug(f"{mat_w.shape = } and {[pt.size for pt in pts] = }.") assert mat_w.shape == tuple([pt.size for pt in pts]) local_nonzero = xp.array(bool(xp.any(xp.abs(mat_w) > 1e-14)), dtype=bool) if self.derham.comm is not None: # Checks if the block is non zero on at least MPI processes self.derham.comm.Allreduce(MPI.IN_PLACE, local_nonzero, op=MPI.LOR) if bool(local_nonzero): if mat_w is None: # The block is globally non-zero, but this rank has a locally zero weight. # We still allocate the block and pass a zero local weight array so that # the local matrix has the same structure as on the other MPI ranks. mat_w = xp.zeros(tuple([pt.size for pt in pts]), dtype=float) if self._matrix_free: blocks[-1] += [ StencilMatrixFreeMassOperator( self.derham, vspace, wspace, weights=loc_weight if loc_weight is not None else mat_w, nquads=self.nquads, ) ] else: blocks[-1] += [ StencilMatrix( vspace.coeff_space, wspace.coeff_space, backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True, ) ] self._weights[-1] += [loc_weight if loc_weight is not None else mat_w] else: blocks[-1] += [None] self._weights[-1] += [None] if len(blocks) == len(blocks[0]) == 1: if blocks[0][0] is None: if self._matrix_free: self._mat = StencilMatrixFreeMassOperator( self.derham, vspace, wspace, nquads=self.nquads, ) else: self._mat = StencilMatrix( vspace.coeff_space, wspace.coeff_space, backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True, ) else: self._mat = blocks[0][0] else: self._mat = BlockLinearOperator( V.coeff_space, W.coeff_space, blocks=blocks, ) # transpose of matrix and weights if transposed: self._mat = self._mat.transpose() n_rows = len(self._weights) n_cols = len(self._weights[0]) tmp_weights = [] for m in range(n_cols): tmp_weights += [[]] for n in range(n_rows): if self._weights[n][m] is not None: tmp_weights[-1] += [self._weights[n][m]] else: tmp_weights[-1] += [None] self._weights = tmp_weights self._W_extraction_op_T = self._W_extraction_op.T self._W_boundary_op_T = self._W_boundary_op.T self._V_extraction_op_T = self._V_extraction_op.T self._V_boundary_op_T = self._V_boundary_op.T # TODO: maybe remove since this is done in the .dot() explicitly # build composite linear operators BW * EW * M * EV^T * BV^T, resp. IDV * EV * M^T * EW^T * IDW^T if self._transposed: self._M = self._V_extraction_op @ self._mat @ self._W_extraction_op_T self._M0 = self._V_boundary_op @ self._M @ self._W_boundary_op_T else: self._M = self._W_extraction_op @ self._mat @ self._V_extraction_op_T self._M0 = self._W_boundary_op @ self._M @ self._V_boundary_op_T # set domain and codomain self._domain = self._M.domain self._codomain = self._M.codomain # allocate temporaries for .dot() self._temp_WB = self._W_boundary_op.domain.zeros() self._temp_WE = self._W_extraction_op.domain.zeros() self._temp_VB = self._V_boundary_op.domain.zeros() self._temp_VE = self._V_extraction_op.domain.zeros() self._temp_mat = self._mat.domain.zeros() # load assembly kernel if not self._matrix_free: self._assembly_kernel = Pyccelkernel( getattr( mass_kernels, "kernel_" + str(self._V.ldim) + "d_mat", ), ) @property def derham(self): return self._derham @property def domain(self): return self._domain @property def domain_symbolic_name(self) -> LiteralOptions.OptsFEECSpace: return self._domain_symbolic_name @property def codomain(self): return self._codomain @property def codomain_symbolic_name(self) -> LiteralOptions.OptsFEECSpace: return self._codomain_symbolic_name @property def name(self): return self._name @property def domain_femspace(self): return self._domain_femspace @property def codomain_femspace(self): return self._codomain_femspace @property def spline_functions(self): return self._spline_functions @property def dtype(self): return self._dtype
[docs] def tosparse(self): if all(op is None for op in (self._W_extraction_op, self._V_extraction_op)): for bl in self._V_boundary_op.bc: for bc in bl: assert not bc, logger.info(".tosparse() only works without boundary conditions at the moment") for bl in self._W_boundary_op.bc: for bc in bl: assert not bc, logger.info(".tosparse() only works without boundary conditions at the moment") return self._mat.tosparse() elif all(isinstance(op, IdentityOperator) for op in (self._W_extraction_op, self._V_extraction_op)): return self._mat.tosparse() else: raise NotImplementedError()
[docs] def toarray(self): if all(op is None for op in (self._W_extraction_op, self._V_extraction_op)): for bl in self._V_boundary_op.bc: for bc in bl: assert not bc, logger.info(".toarray() only works without boundary conditions at the moment") for bl in self._W_boundary_op.bc: for bc in bl: assert not bc, logger.info(".toarray() only works without boundary conditions at the moment") return self._mat.toarray() elif all(isinstance(op, IdentityOperator) for op in (self._W_extraction_op, self._V_extraction_op)): return self._mat.toarray() else: raise NotImplementedError()
@property def M(self): return self._M @property def M0(self): return self._M0 @property def matrix(self): return self._mat @property def nquads(self): if self._nquads is None: return self.derham.nquads else: return self._nquads @property def symmetry(self): return self._symmetry @property def weights(self): return self._weights
[docs] def dot(self, v, out=None, apply_bc=True): """Dot product of the operator with a vector. Parameters ---------- v : feectools.linalg.basic.Vector The input (domain) vector. out : feectools.linalg.basic.Vector, optional If given, the output will be written in-place into this vector. apply_bc : bool Whether to apply the boundary operators (True) or not (False). Returns ------- out : feectools.linalg.basic.Vector The output (codomain) vector. """ assert isinstance(v, Vector) assert v.space == self.domain # newly created output vector if out is None: out = self.codomain.zeros() else: assert isinstance(out, Vector) assert out.space == self.codomain if apply_bc: if self._transposed: self._W_boundary_op_T.dot(v, out=self._temp_WB) self._W_extraction_op_T.dot(self._temp_WB, out=self._temp_mat) self._mat.dot(self._temp_mat, out=self._temp_VE) self._V_extraction_op.dot(self._temp_VE, out=self._temp_VB) out = self._V_boundary_op.dot(self._temp_VB, out=out) else: self._V_boundary_op_T.dot(v, out=self._temp_VB) self._V_extraction_op_T.dot(self._temp_VB, out=self._temp_mat) self._mat.dot(self._temp_mat, out=self._temp_WE) self._W_extraction_op.dot(self._temp_WE, out=self._temp_WB) out = self._W_boundary_op.dot(self._temp_WB, out=out) else: if self._transposed: self._W_extraction_op_T.dot(v, out=self._temp_mat) self._mat.dot(self._temp_mat, out=self._temp_VE) out = self._V_extraction_op.dot(self._temp_VE, out=out) else: self._V_extraction_op_T.dot(v, out=self._temp_mat) self._mat.dot(self._temp_mat, out=self._temp_WE) out = self._W_extraction_op.dot(self._temp_WE, out=out) return out
[docs] def transpose(self, conjugate=False): """ Returns the transposed operator. """ # bring weights back in "right" (not transposed order) if self._transposed: n_rows = len(self._weights) n_cols = len(self._weights[0]) weights = [] for m in range(n_cols): weights += [[]] for n in range(n_rows): if self._weights[n][m] is not None: weights[-1] += [self._weights[n][m]] else: weights[-1] += [None] else: weights = self._weights if self._symmetry is None: M = WeightedMassOperator( self.derham, self._V, self._W, name=self.name + "T", V_extraction_op=self._V_extraction_op, W_extraction_op=self._W_extraction_op, V_boundary_op=self._V_boundary_op, W_boundary_op=self._W_boundary_op, weights_info=weights, transposed=not self._transposed, matrix_free=self._matrix_free, ) M.assemble() else: M = WeightedMassOperator( self.derham, self._V, self._W, name=self.name + "T", V_extraction_op=self._V_extraction_op, W_extraction_op=self._W_extraction_op, V_boundary_op=self._V_boundary_op, W_boundary_op=self._W_boundary_op, weights_info=self._symmetry, transposed=not self._transposed, matrix_free=self._matrix_free, ) M.assemble(weights=weights) return M
[docs] def assemble(self, weights=None, clear=True): r""" Assembles the weighted mass matrix, i.e. computes the integrals .. math:: \mathbb M^{\beta \alpha}_{(\mu,ijk),(\nu,mno)} = \int_{[0, 1]^3} \Lambda^\beta_{\mu,ijk} \, A_{\mu,\nu} \, \Lambda^\alpha_{\nu,mno} \, \textnormal d^3 \boldsymbol\eta\,. The integration is performed with Gauss-Legendre quadrature over the logical domain. Parameters ---------- weights : list | NoneType Weight function(s) (callables or xp.ndarrays) in a 2d list of shape corresponding to number of components of domain/codomain. If ``weights=None``, the weight is taken from the given weights in the instantiation of the object, else it will be overriden. clear : bool Whether to first set all data to zero before assembly. If False, the new contributions are added to existing ones. """ if self._matrix_free: if weights is not None: if self._is_scalar: self._mat.weights = weights[0][0] else: for a, weights_row in enumerate(weights): for b, weight in enumerate(weights_row): if weight is not None: assert callable(weight) or isinstance( weight, xp.ndarray, ) self._mat[a, b].weights = weight self._weights = weights else: # clear data if clear: if isinstance(self._mat, StencilMatrix): self._mat._data[:] = 0.0 else: for block_row in self._mat.blocks: for block in block_row: if block is not None: block._data[:] = 0.0 logger.debug( f'\nAssembling matrix of WeightedMassOperator "{self.name}" with V={self._domain_symbolic_name}, W={self._codomain_symbolic_name}.', ) # collect domain/codomain TensorFemSpaces for each component in tuple if self._transposed: if isinstance(self._W, TensorFemSpace): domain_spaces = (self._W,) else: domain_spaces = self._W.spaces if isinstance(self._V, TensorFemSpace): codomain_spaces = (self._V,) else: codomain_spaces = self._V.spaces else: if isinstance(self._V, TensorFemSpace): domain_spaces = (self._V,) else: domain_spaces = self._V.spaces if isinstance(self._W, TensorFemSpace): codomain_spaces = (self._W,) else: codomain_spaces = self._W.spaces # set new weights and check for compatibility if weights is not None: assert isinstance(weights, list) self._weights = weights V_name = self.domain_symbolic_name W_name = self.codomain_symbolic_name spline_attr = self.derham.spline_attributes # loop over codomain spaces (rows) for a, codomain_space in enumerate(codomain_spaces): # knot span indices of elements of local domain codomain_spans = spline_attr[W_name].quad_grid_spans[a] # global start spline index on process codomain_starts = [int(start) for start in codomain_space.coeff_space.starts] # pads (ghost regions) codomain_pads = codomain_space.coeff_space.pads # quadrature points pts = [points.flatten() for points in spline_attr[W_name].quad_grid_pts[a]] # global quadrature weights in format (local element, local weight) wts = spline_attr[W_name].quad_grid_wts[a] # evaluated basis functions at quadrature points of codomain space codomain_basis = spline_attr[W_name].quad_grid_bases[a] # loop over domain spaces (columns) for b, domain_space in enumerate(domain_spaces): # skip None and redundant blocks (lower half for symmetric and anti-symmetric) if not self._is_scalar: if self._symmetry is not None and a > b: continue loc_weight = self._weights[a][b] # evaluate weight at quadrature points if callable(loc_weight): PTS = xp.meshgrid(*pts, indexing="ij") mat_w = loc_weight(*PTS).copy() elif isinstance(loc_weight, xp.ndarray): mat_w = loc_weight elif loc_weight is not None: raise TypeError( "weights must be callable or xp.ndarray or None but is {}".format( type(self._weights[a][b]), ), ) if loc_weight is not None: assert mat_w.shape == tuple([pt.size for pt in pts]) # evalute splines and multiply for name, spline in self.spline_functions.items(): logger.debug( f"Maximum coefficient of spline {name}: {xp.max(xp.abs(spline.vector.toarray()))}" ) values = spline.eval_tp_fixed_loc( self.spans[name], self.bases[name], out=self.spline_values[name], ) if xp.all(xp.abs(values) < 1e-14): logger.warning( f"The spline weight {name} is close to zero at all quadrature points in the assembly of the weighted mass matrix {self.name}. Weights are not multiplied." ) continue mat_w *= values else: logger.debug(f"No weight for block {a, b}, setting mat_w to None.") mat_w = None not_weight_zero = xp.array( int(loc_weight is not None and xp.any(xp.abs(mat_w) > 1e-14)), ) if self.derham.comm is not None: self.derham.comm.Allreduce( MPI.IN_PLACE, not_weight_zero, op=MPI.LOR, ) # evaluated basis functions at quadrature points of domain space domain_basis = spline_attr[V_name].quad_grid_bases[b] # assemble matrix (if mat_w is not zero) by calling the appropriate kernel (1d, 2d or 3d) if not_weight_zero or self._is_scalar: # get cell of block matrix (don't instantiate if all zeros) if self._is_scalar: mat = self._mat if loc_weight is None: # not_weight_zero is global after the MPI reduction. Hence this rank may # enter the assembly branch even when its own local weight is None. # In that case we assemble a zero local contribution, but we must still # provide a correctly shaped array to the pyccel kernel. mat_w = xp.zeros( tuple([pt.size for pt in pts]), ) else: mat = self._mat[a, b] # block case: after the MPI Allreduce, this block may be globally # non-zero even if it is locally zero on this rank. if mat_w is None: mat_w = xp.zeros(tuple([pt.size for pt in pts])) # This can happen for block matrices if the block was previously considered # zero locally, but is now required because it is non-zero on at least one # MPI rank. The block must exist on all ranks before assembly/exchange. if mat is None: # Maybe in a previous iteration we had more zeros # Can only happen in the Block case self._mat[a, b] = StencilMatrix( domain_space.coeff_space, codomain_space.coeff_space, backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True, ) mat = self._mat[a, b] logger.debug(f"Assemble block {a, b}") self._assembly_kernel( *codomain_spans, *codomain_space.degree, *domain_space.degree, *codomain_starts, *codomain_pads, *wts, *codomain_basis, *domain_basis, mat_w, mat._data, ) else: if clear: self._mat[a, b] = None else: continue # exchange assembly data (accumulate ghost regions) self._mat.exchange_assembly_data() # copy data for symmetric/anti-symmetric block matrices if self.symmetry == "symm": self._mat.update_ghost_regions() self._mat[1, 0]._data[:] = self._mat[0, 1].T._data self._mat[2, 0]._data[:] = self._mat[0, 2].T._data self._mat[2, 1]._data[:] = self._mat[1, 2].T._data elif self.symmetry == "asym": self._mat.update_ghost_regions() self._mat[1, 0]._data[:] = -self._mat[0, 1].T._data self._mat[2, 0]._data[:] = -self._mat[0, 2].T._data self._mat[2, 1]._data[:] = -self._mat[1, 2].T._data logger.debug("Done.")
[docs] def copy(self, out=None): """Create a copy of self, that can potentially be stored in a given WeightedMassOperator. Parameters ---------- out : WeightedMassOperator(optional) The existing WeightedMassOperator in which we want to copy self. """ if out is not None: assert isinstance(out, WeightedMassOperator) assert out.domain is self.domain assert out.codomain is self.codomain else: out = WeightedMassOperator( self.derham, V=self._V, W=self._W, V_extraction_op=self._V_extraction_op, W_extraction_op=self._W_extraction_op, V_boundary_op=self._V_boundary_op, W_boundary_op=self._W_boundary_op, weights_info=self._weights_info, transposed=self._transposed, matrix_free=self._matrix_free, ) self._mat.copy(out=out._mat) return out
def __imul__(self, a): self._mat *= a return self def __iadd__(self, M): assert M.domain is self.domain assert M.codomain is self.codomain if isinstance(M, WeightedMassOperator): self._mat += M._mat return self elif isinstance(M, LinearOperator): self._mat += M return self else: return LinearOperator.__add__(self, M) def __isub__(self, M): assert M.domain is self.domain assert M.codomain is self.codomain if isinstance(M, WeightedMassOperator): self._mat -= M._mat return self elif isinstance(M, LinearOperator): self._mat -= M return self else: return LinOpWithTransp.__sub__(self, M)
[docs] def eval_quad(self, W, coeffs, out=None): """ Evaluates a given FEM field defined by its coefficients at the L2 quadrature points. Parameters ---------- W : TensorFemSpace | VectorFemSpace Tensor product spline space from feectools.fem.tensor. coeffs : StencilVector | BlockVector The coefficient vector corresponding to the FEM field. Ghost regions must be up-to-date! out : xp.ndarray | list/tuple of xp.ndarrays, optional If given, the result will be written into these arrays in-place. Number of outs must be compatible with number of components of FEM field. Returns ------- out : xp.ndarray | list/tuple of xp.ndarrays The values of the FEM field at the quadrature points. """ assert isinstance(W, (TensorFemSpace, VectorFemSpace)) assert isinstance(coeffs, (StencilVector, BlockVector)) assert W.coeff_space == coeffs.space # collect TensorFemSpaces for each component in tuple if isinstance(W, TensorFemSpace): Wspaces = (W,) else: Wspaces = W.spaces # prepare output if out is None: out = () if isinstance(W, TensorFemSpace): out += ( xp.zeros( [ q_grid[nquad].points.size for q_grid, nquad in zip(get_quad_grids(W, nquads=self.nquads), self.nquads) ], dtype=float, ), ) else: for space in W.spaces: out += ( xp.zeros( [ q_grid[nquad].points.size for q_grid, nquad in zip( get_quad_grids(space, nquads=self.nquads), self.nquads, ) ], dtype=float, ), ) else: if isinstance(W, TensorFemSpace): assert isinstance(out, xp.ndarray) out = (out,) else: assert isinstance(out, (list, tuple)) # load assembly kernel kernel = Pyccelkernel(getattr(mass_kernels, "kernel_" + str(W.ldim) + "d_eval")) # loop over components for a, wspace in enumerate(Wspaces): # knot span indices of elements of local domain spans = [ quad_grid[nquad].spans for quad_grid, nquad in zip(get_quad_grids(wspace, nquads=self.nquads), self.nquads) ] # global start spline index on process starts = [int(start) for start in wspace.coeff_space.starts] # pads (ghost regions) pads = wspace.coeff_space.pads # global quadrature points (flattened) and weights in format (local element, local weight) pts = [ quad_grid[nquad].points.flatten() for quad_grid, nquad in zip(get_quad_grids(wspace, nquads=self.nquads), self.nquads) ] wts = [ quad_grid[nquad].weights for quad_grid, nquad in zip(get_quad_grids(wspace, nquads=self.nquads), self.nquads) ] # evaluated basis functions at quadrature points of codomain space basis = [ quad_grid[nquad].basis for quad_grid, nquad in zip(get_quad_grids(wspace, nquads=self.nquads), self.nquads) ] if isinstance(coeffs, StencilVector): kernel( *spans, *wspace.degree, *starts, *pads, *basis, coeffs._data, out[a], ) else: kernel( *spans, *wspace.degree, *starts, *pads, *basis, coeffs[a]._data, out[a], ) if len(out) == 1: return out[0] else: return out
def info(self, use_rst=False): return info(self, use_rst=use_rst)
[docs] class StencilMatrixFreeMassOperator(LinOpWithTransp): r"""Class implementing matrix-free weighted mass operators between StencilVectorSpaces. The result of the dot product with a spline function :math:`S_h` is computed as .. math:: w^\mu_{ijk} = \int \Lambda_{\mu,ijk}\, S_h\, w(\boldsymbol\eta)\,\textrm d \boldsymbol \eta \,, where :math:`w(\boldsymbol\eta)` is a weight function (including the geometric weights). Should only be instanciated via `WeightedMassOperator`, where it's used to replace `StencilMatrix` when one does not want to assemble the matrix for cost reasons Parameters ---------- V : TensorFemSpace Domain of the mass operator W : TensorFemSpace Codomain of the mass operator weights : callable | numpy.ndarry | None The weights of the mass operator """ def __init__(self, derham, V, W, weights=None, nquads=None): self._V = V self._W = W self._domain = V.coeff_space self._codomain = W.coeff_space self._weights = weights self._derham = derham self._nquads = nquads self._dtype = V.coeff_space.dtype self._dot_kernel = Pyccelkernel( getattr( mass_kernels, "kernel_" + str(self._V.ldim) + "d_matrixfree", ), ) self._diag_kernel = Pyccelkernel( getattr( mass_kernels, "kernel_" + str(self._V.ldim) + "d_diag", ), ) shape = tuple(e - s + 1 for s, e in zip(V.coeff_space.starts, V.coeff_space.ends)) self._diag_tmp = xp.zeros((shape)) # knot span indices of elements of local domain self._codomain_spans = [ quad_grid[nquad].spans for quad_grid, nquad in zip(get_quad_grids(self._W, nquads=self.nquads), self.nquads) ] # global start spline index on process self._codomain_starts = [int(start) for start in self._W.coeff_space.starts] # pads (ghost regions) self._codomain_pads = self._W.coeff_space.pads # evaluated basis functions at quadrature points of codomain space self._codomain_basis = [ quad_grid[nquad].basis for quad_grid, nquad in zip(get_quad_grids(self._W, nquads=self.nquads), self.nquads) ] # knot span indices of elements of local domain self._domain_spans = [ quad_grid[nquad].spans for quad_grid, nquad in zip(get_quad_grids(self._V, nquads=self.nquads), self.nquads) ] # global start spline index on process self._domain_starts = [int(start) for start in self._V.coeff_space.starts] # pads (ghost regions) self._domain_pads = self._V.coeff_space.pads # evaluated basis functions at quadrature points of domain space self._domain_basis = [ quad_grid[nquad].basis for quad_grid, nquad in zip(get_quad_grids(self._V, nquads=self.nquads), self.nquads) ] # global quadrature points (flattened) and weights in format (local element, local weight) self._pts = [ quad_grid[nquad].points.flatten() for quad_grid, nquad in zip(get_quad_grids(self._W, nquads=self.nquads), self.nquads) ] self._wts = [ quad_grid[nquad].weights for quad_grid, nquad in zip( get_quad_grids(self._W, nquads=self.nquads), self.nquads, ) ] @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def dtype(self): return self._dtype @property def nquads(self): if self._nquads is None: return self.derham.nquads else: return self._nquads @property def derham(self): """Discrete de Rham sequence on the logical unit cube.""" return self._derham @property def tosparse(self): raise NotImplementedError() @property def toarray(self): raise NotImplementedError()
[docs] def transpose(self, conjugate=False): return StencilMatrixFreeMassOperator( self._derham, self._codomain, self._domain, self._weights, nquads=self._nquads, )
@property def weights(self): return self._weights @weights.setter def weights(self, new): self._weights = new
[docs] def dot(self, v, out=None): """ Dot product of the operator with a vector. Direct computation (not using a StencilMatrix). Parameters ---------- v : feectools.linalg.basic.Vector The input (domain) vector. out : feectools.linalg.basic.Vector, optional If given, the output will be written in-place into this vector. apply_bc : bool Whether to apply the boundary operators (True) or not (False). Returns ------- out : feectools.linalg.basic.Vector The output (codomain) vector. """ if out is None: out = self.codomain.zeros() else: assert isinstance(out, Vector) assert out.space == self.codomain out._data[:] = 0.0 v.update_ghost_regions() # evaluate weight at quadrature points if callable(self._weights): PTS = xp.meshgrid(*self._pts, indexing="ij") mat_w = self._weights(*PTS).copy() elif isinstance(self._weights, xp.ndarray): mat_w = self._weights if self._weights is not None: assert mat_w.shape == tuple([pt.size for pt in self._pts]) # call kernel (if mat_w is not zero) by calling the appropriate kernel (1d, 2d or 3d) if xp.any(xp.abs(mat_w) > 1e-14): self._dot_kernel( *self._codomain_spans, *self._domain_spans, *self._W.degree, *self._V.degree, *self._codomain_starts, *self._domain_starts, *self._codomain_pads, *self._domain_pads, *self._wts, *self._codomain_basis, *self._domain_basis, mat_w, out._data, v._data, ) out.exchange_assembly_data() return out
[docs] def diagonal(self, inverse=False, sqrt=False, out=None): """ Get the coefficients on the main diagonal as a StencilDiagonalMatrix object. Parameters ---------- inverse : bool If True, get the inverse of the diagonal. (Default: False). sqrt : bool If True, get the square root of the diagonal. (Default: False). Can be combined with inverse to get the inverse square root out : StencilDiagonalMatrix If provided, write the diagonal entries into this matrix. (Default: None). Returns ------- StencilDiagonalMatrix The matrix which contains the main diagonal of self (or its inverse). """ # Check `inverse` argument assert isinstance(inverse, bool) # Only if domain == codomain assert self.domain == self.codomain # Determine domain and codomain of the StencilDiagonalMatrix V, W = self.domain, self.codomain # Check `out` argument if out is not None: assert isinstance(out, StencilDiagonalMatrix) assert out.domain is V assert out.codomain is W # evaluate weight at quadrature points if callable(self._weights): PTS = xp.meshgrid(*self._pts, indexing="ij") mat_w = self._weights(*PTS).copy() elif isinstance(self._weights, xp.ndarray): mat_w = self._weights diag = self._diag_tmp diag[:] = 0.0 self._diag_kernel( *self._codomain_spans, *self._W.degree, *self._codomain_starts, *self._codomain_pads, *self._wts, *self._codomain_basis, mat_w, diag, ) data = out._data if out else None # Calculate entries of StencilDiagonalMatrix if sqrt: diag = xp.sqrt(diag) if inverse: data = xp.divide(1, diag, out=data) elif out: xp.copyto(data, diag) else: data = diag.copy() # If needed create a new StencilDiagonalMatrix object if out is None: out = StencilDiagonalMatrix(V, W, data) return out
[docs] class L2Projector: r""" An orthogonal projection into a discrete :class:`~struphy.feec.psydac_derham.Derham` space based on the L2-scalar product. It solves the following system for the FE-coefficients :math:`\mathbf f = (f_{lmn}) \in \mathbb R^{N_\alpha}`: .. math:: \mathbb M^\alpha_{ijk, lmn} f_{lmn} = (f^\alpha, \Lambda^\alpha_{ijk})_{L^2}\,, where :math:`\mathbb M^\alpha` denotes the :ref:`mass matrix <weighted_mass>` of space :math:`\alpha \in \{0,1,2,3,v\}` and :math:`f^\alpha` is a :math:`\alpha`-form proxy function. Parameters: ----------- space_id : str One of "H1", "Hcurl", "Hdiv", "L2" or "H1vec". mass_ops : struphy.mass.WeighteMassOperators Mass operators object, see :ref:`mass_ops`. solver : LiteralOptions.OptsSymmSolver, default="pcg" Symmetric iterative solver used by implicit or explicit operators. precond : LiteralOptions.OptsMassPrecond, default="MassMatrixPreconditioner" Preconditioner for the mass-matrix block. solver_params : SolverParameters, default=None Solver controls; defaults to ``SolverParameters()``. """ def __init__( self, space_id: str, mass_ops: WeightedMassOperators, solver_name: LiteralOptions.OptsSymmSolver = "pcg", precond_name: LiteralOptions.OptsMassPrecond = "MassMatrixPreconditioner", solver_params: SolverParameters = None, ): assert space_id in ("H1", "Hcurl", "Hdiv", "L2", "H1vec") # TODO: enable serialization of WeightedMassOperators # self.params = copy.deepcopy(locals()) # TODO: move L2projector to its own file and avoid circular imports from struphy.feec import preconditioner if solver_params is None: solver_params = SolverParameters() self._space_id = space_id self._mass_ops = mass_ops self._space_key = mass_ops.derham.space_to_form[self.space_id] self._space = mass_ops.derham.fem_spaces[self.space_key] # mass matrix self._Mmat = getattr(self.mass_ops, "M" + self.space_key) # quadrature grid self._quad_grid_pts = self.mass_ops.derham.spline_attributes[self.space_key].quad_grid_pts if space_id in ("H1", "L2"): self._quad_grid_mesh = xp.meshgrid( *[pt.flatten() for pt in self.quad_grid_pts[0]], indexing="ij", ) self._geom_weights = self.Mmat.weights[0][0] # (*self.quad_grid_mesh) else: self._quad_grid_mesh = [] self._tmp = [] # tmp for matrix-vector product of geom_weights with fun for pts in self.quad_grid_pts: self._quad_grid_mesh += [ xp.meshgrid( *[pt.flatten() for pt in pts], indexing="ij", ), ] self._tmp += [xp.zeros_like(self.quad_grid_mesh[-1][0])] # geometric weights evaluated at quadrature grid self._geom_weights = [] # loop over rows (different meshes) for mesh, row_weights in zip(self.quad_grid_mesh, self.Mmat.weights): self._geom_weights += [[]] # loop over columns (differnt geometric coeffs) for weight in row_weights: if weight is not None: self._geom_weights[-1] += [weight] # (*mesh)] else: self._geom_weights[-1] += [xp.zeros_like(mesh[0])] # other quad grid info self._tensor_fem_spaces = self.mass_ops.derham.spline_attributes[self.space_key].tensor_spaces self._wts_l = self.mass_ops.derham.spline_attributes[self.space_key].quad_grid_wts self._spans_l = self.mass_ops.derham.spline_attributes[self.space_key].quad_grid_spans self._bases_l = self.mass_ops.derham.spline_attributes[self.space_key].quad_grid_bases # Preconditioner if precond_name is None: pc = None else: pc_class = getattr(preconditioner, precond_name) pc = pc_class(self.Mmat) # solver self._solver = inverse( self.Mmat, solver_name, pc=pc, tol=solver_params.tol, maxiter=solver_params.maxiter, verbose=solver_params.verbose, ) @property def params(self) -> dict: """Parameters passed to __init__(), as dictionary.""" if not hasattr(self, "_params"): self._params = {} return self._params @params.setter def params(self, new): assert isinstance(new, dict) if "self" in new: new.pop("self") if "__class__" in new: new.pop("__class__") self._params = new @property def mass_ops(self) -> WeightedMassOperators: """Struphy mass operators object, see :ref:`mass_ops`..""" return self._mass_ops @property def space_id(self) -> str: """The ID of the space (H1, Hcurl, Hdiv, L2 or H1vec).""" return self._space_id @property def space_key(self) -> str: """The key of the space (0, 1, 2, 3 or v).""" return self._space_key @property def space(self) -> FemSpace: """The Derham finite element space (from ``Derham.fem_spaces``).""" return self._space @property def solver(self) -> InverseLinearOperator: """The iterative solver for the mass matrix.""" return self._solver @property def Mmat(self) -> WeightedMassOperator: """The mass matrix of space.""" return self._Mmat @property def quad_grid_pts(self) -> tuple[tuple[xp.ndarray]]: """List of quadrature points in each direction for integration over grid cells in format (ni, nq) = (cell, quadrature point).""" return self._quad_grid_pts @property def quad_grid_mesh(self) -> list[tuple[xp.ndarray]]: """Mesh grids of quad_grid_pts.""" return self._quad_grid_mesh @property def geom_weights(self) -> list[list[xp.ndarray]]: """Geometric coefficients (e.g. Jacobians) evaluated at quad_grid_mesh, stored as list[list] either 1x1 or 3x3.""" return self._geom_weights
[docs] def __repr__(self): out = f"{self.__class__.__name__}(\n" for k, v in self.params.items(): out += " " * 4 out += f"{k}={v},\n" out += ")" return out
def __repr_no_defaults__(self): return __class_with_params_repr_no_defaults__(self)
[docs] def solve(self, rhs: StencilVector | BlockVector, out=None) -> StencilVector | BlockVector: """ Solves the linear system M * x = rhs, where M is the mass matrix. Parameters ---------- rhs : feectools.linalg.basic.vector The right-hand side of the linear system. out : feectools.linalg.basic.vector, optional If given, the result will be written into this vector in-place. Returns ------- out : feectools.linalg.basic.vector Output vector (result of linear system). """ assert isinstance(rhs, StencilVector) or isinstance(rhs, BlockVector) assert rhs.space == self.Mmat.domain if out is None: out = self.solver.dot(rhs) else: self.solver.dot(rhs, out=out) return out
[docs] def get_dofs( self, fun: Callable | xp.ndarray | list[Callable | xp.ndarray] | tuple[Callable | xp.ndarray], dofs: StencilVector | BlockVector = None, apply_bc: bool = False, clear: bool = True, ) -> StencilVector | BlockVector: r""" Assembles (in 3d) the Stencil-/BlockVector .. math:: V_{ijk} = \int f * w_\textrm{geom} * \Lambda^\alpha_{ijk}\,\textrm d \boldsymbol \eta = \left( f\,, \Lambda^\alpha_{ijk}\right)_{L^2}\,, where :math:`\Lambda^\alpha_{ijk}` are the basis functions of :math:`V_h^\alpha`, :math:`f` is an :math:`\alpha`-form proxy function and :math:`w_\textrm{geom}` stand for metric coefficients. Note that any geometric terms (e.g. Jacobians) in the L2 scalar product are automatically assembled into :math:`w_\textrm{geom}`, depending on the space of :math:`\alpha`-forms. The integration is performed with Gauss-Legendre quadrature over the whole logical domain. Parameters ---------- fun : Callable | xp.ndarray | list[Callable | xp.ndarray] | tuple[Callable | xp.ndarray] Weight function(s) (callables or xp.ndarrays) in a 1d list of shape corresponding to number of components. dofs : StencilVector | BlockVector, optional The vector for the output. apply_bc : bool, optional Whether to apply essential boundary conditions to degrees of freedom. clear : bool, optional Whether to first set all data to zero before assembly. If False, the new contributions are added to existing ones in vec. Returns ------- dofs : StencilVector | BlockVector The assembled degrees of freedom, before projection. """ # evaluate fun at quad_grid or check array size if callable(fun): fun_weights = fun(*self.quad_grid_mesh) elif isinstance(fun, xp.ndarray): assert fun.shape == self.quad_grid_mesh[0].shape, ( f"Expected shape {self.quad_grid_mesh[0].shape}, got {fun.shape =} instead." ) fun_weights = fun else: assert ( len( fun, ) == 3 ), f"List input only for vector-valued spaces of size 3, but {len(fun) =}." fun_weights = [] # loop over rows (different meshes) for mesh in self.quad_grid_mesh: fun_weights += [[]] # loop over columns (different functions) for f in fun: if callable(f): fun_weights[-1] += [f(*mesh)] elif isinstance(f, xp.ndarray): assert f.shape == mesh[0].shape, f"Expected shape {mesh[0].shape}, got {f.shape =} instead." fun_weights[-1] += [f] else: raise ValueError( f"Expected callable or numpy array, got {type(f) =} instead.", ) # check output vector if dofs is None: dofs = self.space.coeff_space.zeros() else: assert isinstance(dofs, (StencilVector, BlockVector, PolarVector)) assert dofs.space == self.Mmat.codomain # compute matrix data for kernel, i.e. fun * geom_weight tot_weights = [] if isinstance(fun_weights, xp.ndarray): tot_weights += [fun_weights * self.geom_weights] else: # loop over rows (differnt meshes) for row_fun, row_geom, tmp in zip(fun_weights, self.geom_weights, self._tmp): tmp *= 0.0 # loop over columns (different functions) for fun_weight, geom_weight in zip(row_fun, row_geom): # matrix-vector product tmp += fun_weight * geom_weight tot_weights += [tmp] # clear data if clear: if isinstance(dofs, StencilVector): dofs._data[:] = 0.0 elif isinstance(dofs, PolarVector): dofs.tp._data[:] = 0.0 else: for block in dofs.blocks: block._data[:] = 0.0 # loop over components (just one for scalar spaces) for a, (fem_space, spans, wts, basis, mat_w) in enumerate( zip( self._tensor_fem_spaces, self._spans_l, self._wts_l, self._bases_l, tot_weights, ), ): # indices starts = [int(start) for start in fem_space.coeff_space.starts] pads = fem_space.coeff_space.pads if isinstance(dofs, StencilVector): mass_kernels.kernel_3d_vec( *spans, *fem_space.degree, *starts, *pads, *wts, *basis, mat_w, dofs._data, ) elif isinstance(dofs, PolarVector): mass_kernels.kernel_3d_vec( *spans, *fem_space.degree, *starts, *pads, *wts, *basis, mat_w, dofs.tp._data, ) else: mass_kernels.kernel_3d_vec( *spans, *fem_space.degree, *starts, *pads, *wts, *basis, mat_w, dofs[a]._data, ) # exchange assembly data (accumulate ghost regions) and update ghost regions dofs.exchange_assembly_data() dofs.update_ghost_regions() # apply boundary operator if apply_bc: dofs = self.mass_ops.derham.boundary_ops[self.space_key].dot(dofs) return dofs
[docs] def __call__( self, fun: Callable | list[Callable] | tuple[Callable], out: StencilVector | BlockVector = None, dofs: StencilVector | BlockVector = None, apply_bc: bool = False, ) -> StencilVector | BlockVector: """ Applies projector to given callable(s). Parameters ---------- fun : Callable | list[Callable] | tuple[Callable] The function to be projected. List of three callables for vector-valued functions. out : StencilVector | BlockVector, optional If given, the result will be written into this vector in-place. dofs : StencilVector | BlockVector, optional If given, the dofs will be written into this vector in-place. apply_bc : bool, optional Whether to apply essential boundary conditions to degrees of freedom and coefficients. Returns ------- coeffs : feectools.linalg.basic.vector The FEM spline coefficients after projection. """ return self.solve(self.get_dofs(fun, dofs=dofs, apply_bc=apply_bc), out=out)
[docs] class AverageOperator(LinOpWithTransp): r""" Class for quadrature operators, performs the average of a `FeecVariable` along a given direction. For example along the :math:`\eta_3` direction, it applies the following linear operator : .. math:: \mathbb M^{\alpha}_{(\mu,ijk),(\nu,mno)} = \delta_{i,m} \delta_{j,n} c_o with :math:`c_o=\int_0^1 N_{o}(\eta_3) \textnormal{d} \eta` and :math:`N_{o}` the B-spline function at the place `o`. In other words, it maps a spline function :math:`S_h` to the function obtained by averaging :math:`S_h` along the given direction, i.e. for the example of direction 3: .. math:: S_h(\eta_1, \eta_2, \eta_3) = \sum_{mno} c_{mno} N_m(\eta_1) N_n(\eta_2) N_o(\eta_3) \quad \mapsto \quad \overline S_h(\eta_1, \eta_2, \eta_3) = \sum_{ijk} \overline c_{ij} N_i(\eta_1) N_j(\eta_2) N_k(\eta_3)\,, with .. math:: \overline c_{ij} = \sum_o \delta_{i,m} \delta_{j,n} \, c_{mno} \int_0^1 N_{o}(\eta_3) \textnormal{d} \eta_3 . Parameters ---------- derham : Derham The derham complexe that supports the space used space : str Identifier of the space on which the average is performed, either `"H1"`, `"Hcurl"`, `"Hdiv"`, `"L2"`, `"H1vec"` direction : int, optional The direction of the space along which the average is performed, either `0`, `1` or `2`. transposed : bool, optional Whether to take the transpose of the operator. """ def __init__( self, derham: Derham, space: str = "H1", direction: int = 2, transposed: bool = False, ): if space not in derham.space_to_form: AssertionError("Must match a space of the derham complex") if space != "H1": NotImplementedError() space_id = "V" + derham.space_to_form[space] self._V = getattr(derham, space_id) # StencilVectorSpace self._domain = getattr(derham, space_id) self._codomain = getattr(derham, space_id) self._pads = self._V.pads # gets the number of ghost cells self._derham = derham self._dtype = self._domain.dtype self._transposed = transposed if direction == 0: self._directions = (0, 1, 2) elif direction == 1: self._directions = (1, 0, 2) elif direction == 2: self._directions = (2, 0, 1) else: raise ValueError("invalid direction id, must be 0, 1 or 2") comm = derham.comm # Selection of ranks for each subcomms regarding their position in the two perpendicular directions to the averaged direction. if not isinstance(comm, (MockComm, type(None))): rank = comm.Get_rank() nprocs = derham.domain_decomposition.nprocs dom_arr = derham.domain_array color1 = int(dom_arr[rank, 3 * self._directions[1]] * nprocs[self._directions[1]]) color2 = int(dom_arr[rank, 3 * self._directions[2]] * nprocs[self._directions[2]]) color = color1 * nprocs[self._directions[2]] + color2 self.subcomm = comm.Split(color=color, key=rank) # We allocate memory for the 2D temporary array for each process self._tmp = xp.zeros( ( int(self._V.ends[self._directions[1]] - self._V.starts[self._directions[1]] + 1), int(self._V.ends[self._directions[2]] - self._V.starts[self._directions[2]] + 1), ) ) # We allocate memory for the weights (integrals of 1D B-splines) self._weights = xp.zeros(int(self._V.ends[self._directions[0]] - self._V.starts[self._directions[0]] + 1)) self.allocate() # definition of subscripts for function xp.einsum if self._transposed: if self._directions[0] == 0: self._subscripts = ("ijk->jk", "ij,o->oij") if self._directions[0] == 1: self._subscripts = ("ijk->ik", "ij,o->ioj") if self._directions[0] == 2: self._subscripts = ("ijk->ij", "ij,o->ijo") else: if self._directions[0] == 0: self._subscripts = ("ojk,o->jk",) if self._directions[0] == 1: self._subscripts = ("iok,o->ik",) if self._directions[0] == 2: self._subscripts = ("ijo,o->ij",) # definition of slices sl_ghost = tuple(slice(p, -p) if p > 0 else slice(None) for p in self._pads) sl_broadcasting = [slice(None), slice(None), slice(None)] sl_broadcasting[self._directions[0]] = None self._slices = (sl_ghost, tuple(sl_broadcasting))
[docs] def allocate(self): """Compute the weights, which are the integrals of 1D B-splines in the averaged direction""" knots = getattr(self.derham.args_derham, "tn" + str(self._directions[0] + 1)) degree = self.derham.degree[self._directions[0]] i_begin, i_end = self._V.starts[self._directions[0]], self._V.ends[self._directions[0]] + 1 if self.derham.bcs[self._directions[0]] is None: i_begin += self.derham.degree[self._directions[0]] i_end += self.derham.degree[self._directions[0]] # General formula for any distribution of knots for the integral of a B-spline function, thus works with periodic and clamped boundary conditions : self._weights[:] = (knots[i_begin + degree + 1 : i_end + degree + 1] - knots[i_begin:i_end]) / (degree + 1)
@property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def dtype(self): return self._dtype @property def derham(self): return self._derham @property def nquads(self): if self._nquads is None: return self.derham.nquads else: return self._nquads @property def tosparse(self): raise NotImplementedError() @property def toarray(self): raise NotImplementedError()
[docs] def dot(self, v, out=None): # assert isinstance(v, StencilVector) # assert v.space == self.domain v.update_ghost_regions() if out is None: out = self.codomain.zeros() x = v._data[self._slices[0]] y = out._data[self._slices[0]] if self._transposed: xp.einsum(self._subscripts[0], x, out=self._tmp) if not isinstance(self.derham.comm, (MockComm, type(None))): self.subcomm.Allreduce(MPI.IN_PLACE, self._tmp, MPI.SUM) xp.einsum(self._subscripts[1], self._tmp, self._weights, out=y) else: xp.einsum(self._subscripts[0], x, self._weights, out=self._tmp) if not isinstance(self.derham.comm, (MockComm, type(None))): self.subcomm.Allreduce(MPI.IN_PLACE, self._tmp, MPI.SUM) y[:] = self._tmp[self._slices[1]] return out
[docs] def transpose(self, conjugate=False): return AverageOperator(self.derham, self.domain, self._weights, transposed=not self._transposed)