import logging
import cunumpy as xp
from feectools.api.settings import PSYDAC_BACKEND_GPYCCEL
from feectools.ddm.mpi import mpi as MPI
from feectools.fem.basic import FemSpace
from feectools.fem.tensor import TensorFemSpace
from feectools.linalg.basic import IdentityOperator, LinearOperator, Vector
from feectools.linalg.block import BlockLinearOperator, BlockVector, BlockVectorSpace
from feectools.linalg.stencil import StencilMatrix, StencilVector, StencilVectorSpace
from struphy.feec import basis_projection_kernels
from struphy.feec.linear_operators import BoundaryOperator, LinOpWithTransp
from struphy.feec.local_projectors_kernels import assemble_basis_projection_operator_local
from struphy.feec.projectors import CommutingProjector, CommutingProjectorLocal
from struphy.feec.psydac_derham import Derham, get_pts_and_wts, get_span_and_basis
from struphy.feec.utilities import LocalRotationMatrix
from struphy.geometry.base import Domain
from struphy.polar.basic import PolarDerhamSpace, PolarVector
from struphy.polar.linear_operators import PolarExtractionOperator
from struphy.utils.docstring_converter import auto_convert_docstring
from struphy.utils.pyccel import Pyccelkernel
logger = logging.getLogger("struphy")
[docs]
class BasisProjectionOperators:
r"""
Collection of pre-defined :class:`struphy.feec.basis_projection_ops.BasisProjectionOperator`.
Parameters
----------
derham : struphy.feec.psydac_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.
**weights : dict
Objects to access callables that can serve as weight functions.
Note
----
Possible choices for key-value pairs in ****weights** are, at the moment:
- eq_mhd: :class:`struphy.fields_background.base.MHDequilibrium`
"""
def __init__(self, derham, domain, **weights):
self._derham = derham
self._domain = domain
self._weights = weights
self._rank = derham.comm.Get_rank() if derham.comm is not None else 0
if xp.any(
[degree == 1 and num_elements > 1 for degree, num_elements in zip(derham.degree, derham.num_elements)]
):
logger.warning(
f'WARNING: Class "BasisProjectionOperators" called with degree={derham.degree} (interpolation of piece-wise constants should be avoided).',
)
@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 weights(self):
"""Dictionary of objects that provide access to callables that can serve as weight functions."""
return self._weights
@property
def rank(self) -> int:
"""MPI rank, is 0 if no communicator."""
return self._rank
# Wrapper functions for evaluating metric coefficients in right order (3x3 entries are last two axes!!)
[docs]
def DF(self, e1, e2, e3):
"""Jacobian callable."""
return self.domain.jacobian(e1, e2, e3, transposed=False, change_out_order=True, squeeze_out=False)
[docs]
def DFT(self, e1, e2, e3):
"""Jacobain transpose callable."""
return self.domain.jacobian(e1, e2, e3, transposed=True, change_out_order=True, squeeze_out=False)
[docs]
def DFinv(self, e1, e2, e3):
"""Jacobain inverse callable."""
return self.domain.jacobian_inv(e1, e2, e3, transposed=False, change_out_order=True, squeeze_out=False)
[docs]
def DFinvT(self, e1, e2, e3):
"""Jacobian inverse transpose callable."""
return self.domain.jacobian_inv(e1, e2, e3, transposed=True, change_out_order=True, squeeze_out=False)
[docs]
def G(self, e1, e2, e3):
"""Metric tensor callable."""
return self.domain.metric(e1, e2, e3, change_out_order=True, squeeze_out=False)
[docs]
def Ginv(self, e1, e2, e3):
"""Inverse metric tensor callable."""
return self.domain.metric_inv(e1, e2, e3, change_out_order=True, squeeze_out=False)
[docs]
def sqrt_g(self, e1, e2, e3):
"""Jacobian determinant callable."""
return abs(self.domain.jacobian_det(e1, e2, e3, squeeze_out=False))
@property
def K0(self):
r"""Basis projection operator
.. math::
\mathcal{K}^{0}_{ijk,mno} := \hat{\Pi}^0_{ijk} \left[ \hat{p}^0_{\text{eq}} \mathbf{\Lambda}^0_{mno} \right] \,.
"""
if not hasattr(self, "_K0"):
fun = [[lambda e1, e2, e3: self.weights["eq_mhd"].p0(e1, e2, e3)]]
self._K0 = self.create_basis_op(
fun,
"H1",
"H1",
name="K0",
)
return self._K0
@property
def K3(self):
r"""Basis projection operator
.. math::
\mathcal{K}^3_{ijk,mno} := \hat{\Pi}^3_{ijk} \left[ \frac{\hat{p}^3_{\text{eq}}}{\sqrt{g}}\Lambda^3_{mno} \right] \,.
"""
if not hasattr(self, "_K3"):
fun = [
[
lambda e1, e2, e3: (
self.weights["eq_mhd"].p3(
e1,
e2,
e3,
)
/ self.sqrt_g(e1, e2, e3)
),
],
]
self._K3 = self.create_basis_op(
fun,
"L2",
"L2",
name="K3",
)
return self._K3
@property
def Qv(self):
r"""Basis projection operator
.. math::
\mathcal{Q}^v_{(\mu,ijk),(\nu,mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[\hat{\rho}^3_{\text{eq}} \Lambda^{0,\nu}_{mno} \right] \,.
"""
if not hasattr(self, "_Qv"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.weights["eq_mhd"].n3(e1, e2, e3) if m == n else 0 * e1,
]
self._Qv = self.create_basis_op(
fun,
"H1vec",
"Hdiv",
name="Qv",
)
return self._Qv
@property
def Q1(self):
r"""Basis projection operator
.. math::
\mathcal{Q}^1_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[\hat{\rho}^3_{\text{eq}}G^{-1}_{\mu,\nu}\Lambda^1_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_Q1"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: (
self.weights["eq_mhd"].n3(e1, e2, e3) * self.Ginv(e1, e2, e3)[:, :, :, m, n]
),
]
self._Q1 = self.create_basis_op(
fun,
"Hcurl",
"Hdiv",
name="Q1",
)
return self._Q1
@property
def Q2(self):
r"""Basis projection operator
.. math::
\mathcal{Q}^2_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[\frac{\hat{\rho}^3_{\text{eq}}}{\sqrt{g}}\Lambda^2_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_Q2"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: (
self.weights["eq_mhd"].n3(
e1,
e2,
e3,
)
/ self.sqrt_g(e1, e2, e3)
if m == n
else 0 * e1
),
]
self._Q2 = self.create_basis_op(
fun,
"Hdiv",
"Hdiv",
name="Q2",
)
return self._Q2
@property
def Q3(self):
r"""Basis projection operator
.. math::
\mathcal{Q}^3_{ijk,mno} := \hat{\Pi}^3_{ijk} \left[ \frac{\hat{\rho}^3_{\text{eq}}}{\sqrt{g}}\Lambda^3_{mno} \right] \,.
"""
if not hasattr(self, "_Q3"):
fun = [
[
lambda e1, e2, e3: (
self.weights["eq_mhd"].n3(
e1,
e2,
e3,
)
/ self.sqrt_g(e1, e2, e3)
),
],
]
self._Q3 = self.create_basis_op(
fun,
"L2",
"L2",
name="Q3",
)
return self._Q3
@property
def Tv(self):
r"""Basis projection operator
.. math::
\mathcal{T}^v_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^1_{(\mu, ijk)} \left[\mathcal R^B_{\mu,\nu} \Lambda^0_{(\nu, mno)} \right] \,.
with the rotation matrix
.. math::
\mathcal R^B_{\mu, \nu} := \epsilon_{\mu \alpha \nu}\, B^2_{\textnormal{eq}, \alpha}\,,\qquad s.t. \qquad \mathcal R^B \vec v = \vec B^2_{\textnormal{eq}} \times \vec v\,,
where :math:`\epsilon_{\mu \alpha \nu}` stands for the Levi-Civita tensor and :math:`B^2_{\textnormal{eq}, \alpha}` is the :math:`\alpha`-component of the MHD equilibrium magnetic field (2-form).
"""
if not hasattr(self, "_Tv"):
rot_B = LocalRotationMatrix(
self.weights["eq_mhd"].b2_1,
self.weights["eq_mhd"].b2_2,
self.weights["eq_mhd"].b2_3,
)
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: rot_B(e1, e2, e3)[:, :, :, m, n],
]
self._Tv = self.create_basis_op(
fun,
"H1vec",
"Hcurl",
name="Tv",
)
return self._Tv
@property
def T1(self):
r"""Basis projection operator
.. math::
\mathcal{T}^1_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^1_{(\mu, ijk)} \left[\mathcal R^B_{\mu, \beta}G^{-1}_{\beta, \nu}\Lambda^1_{(\nu, mno)} \right] \,,
with the rotation matrix
.. math::
\mathcal R^B_{\mu, \beta} := \epsilon_{\mu \alpha \beta}\, B^2_{\textnormal{eq}, \alpha}\,,\qquad s.t. \qquad \mathcal R^B \vec v = \vec B^2_{\textnormal{eq}} \times \vec v\,,
where :math:`\epsilon_{\mu \alpha \beta}` stands for the Levi-Civita tensor and :math:`B^2_{\textnormal{eq}, \alpha}` is the :math:`\alpha`-component of the MHD equilibrium magnetic field (2-form).
"""
if not hasattr(self, "_T1"):
rot_B = LocalRotationMatrix(
self.weights["eq_mhd"].b2_1,
self.weights["eq_mhd"].b2_2,
self.weights["eq_mhd"].b2_3,
)
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: (rot_B(e1, e2, e3) @ self.Ginv(e1, e2, e3))[:, :, :, m, n],
]
self._T1 = self.create_basis_op(
fun,
"Hcurl",
"Hcurl",
name="T1",
)
return self._T1
@property
def T2(self):
r"""Basis projection operator
.. math::
\mathcal{T}^2_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^1_{(\mu, ijk)} \left[\frac{\mathcal R^B_{\mu, \nu}}{\sqrt{g}}\Lambda^2_{(\nu, mno)} \right] \,.
with the rotation matrix
.. math::
\mathcal R^B_{\mu, \nu} := \epsilon_{\mu \alpha \nu}\, B^2_{\textnormal{eq}, \alpha}\,,\qquad s.t. \qquad \mathcal R^B \vec v = \vec B^2_{\textnormal{eq}} \times \vec v\,,
where :math:`\epsilon_{\mu \alpha \nu}` stands for the Levi-Civita tensor and :math:`B^2_{\textnormal{eq}, \alpha}` is the :math:`\alpha`-component of the MHD equilibrium magnetic field (2-form).
"""
if not hasattr(self, "_T2"):
rot_B = LocalRotationMatrix(
self.weights["eq_mhd"].b2_1,
self.weights["eq_mhd"].b2_2,
self.weights["eq_mhd"].b2_3,
)
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: rot_B(e1, e2, e3)[:, :, :, m, n] / self.sqrt_g(e1, e2, e3),
]
self._T2 = self.create_basis_op(
fun,
"Hdiv",
"Hcurl",
name="T2",
)
return self._T2
@property
def Sv(self):
r"""Basis projection operator
.. math::
\mathcal{S}^v_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[\hat{p}^3_{\text{eq}} \Lambda^{0,\nu}_{mno} \right] \,.
"""
if not hasattr(self, "_Sv"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.weights["eq_mhd"].p3(e1, e2, e3) if m == n else 0 * e1,
]
self._Sv = self.create_basis_op(
fun,
"H1vec",
"Hdiv",
name="Sv",
)
return self._Sv
@property
def S1(self):
r"""Basis projection operator
.. math::
\mathcal{S}^1_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[\hat{p}^3_{\text{eq}}G^{-1}_{\mu,\nu}\Lambda^1_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_S1"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: (
self.weights["eq_mhd"].p3(e1, e2, e3) * self.Ginv(e1, e2, e3)[:, :, :, m, n]
),
]
self._S1 = self.create_basis_op(
fun,
"Hcurl",
"Hdiv",
name="S1",
)
return self._S1
@property
def S2(self):
r"""Basis projection operator
.. math::
\mathcal{S}^2_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[\frac{\hat{p}^3_{\text{eq}}}{\sqrt{g}}\Lambda^2_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_S2"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: (
self.weights["eq_mhd"].p3(
e1,
e2,
e3,
)
/ self.sqrt_g(e1, e2, e3)
if m == n
else 0 * e1
),
]
self._S2 = self.create_basis_op(
fun,
"Hdiv",
"Hdiv",
name="S2",
)
return self._S2
@property
def S11(self):
r"""Basis projection operator
.. math::
\mathcal{S}^{11}_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^1_{(\mu, ijk)} \left[ \hat{p}^0_{\text{eq}} \Lambda^1_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_S11"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.weights["eq_mhd"].p0(e1, e2, e3) if m == n else 0 * e1,
]
self._S11 = self.create_basis_op(
fun,
"Hcurl",
"Hcurl",
name="S11",
)
return self._S11
@property
def S21(self):
r"""Basis projection operator
.. math::
\mathcal{S}^{21}_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^1_{(\mu, ijk)} \left[ \frac{G_{\mu, \nu}}{\sqrt{g}} \Lambda^2_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_S21"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.G(e1, e2, e3)[:, :, :, m, n] / self.sqrt_g(e1, e2, e3),
]
self._S21 = self.create_basis_op(
fun,
"Hdiv",
"Hcurl",
name="S21",
)
return self._S21
@property
def S21p(self):
r"""Basis projection operator
.. math::
\mathcal{S}^{21p}_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^1_{(\mu, ijk)} \left[ \frac{G_{\mu, \nu}}{\sqrt{g}} \Lambda^2_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_S21p"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: (
self.weights["eq_mhd"].p0(
e1,
e2,
e3,
)
* self.G(e1, e2, e3)[:, :, :, m, n]
/ self.sqrt_g(e1, e2, e3)
),
]
self._S21p = self.create_basis_op(
fun,
"Hdiv",
"Hcurl",
name="S21p",
)
return self._S21p
@property
def Uv(self):
r"""Basis projection operator
.. math::
\mathcal{U}^v_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[\sqrt{g} \, \Lambda^{0, \nu}_{mno} \right] \,.
"""
if not hasattr(self, "_Uv"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.sqrt_g(e1, e2, e3) if m == n else 0 * e1,
]
self._Uv = self.create_basis_op(
fun,
"H1vec",
"Hdiv",
name="Uv",
)
return self._Uv
@property
def U1(self):
r"""Basis projection operator
.. math::
\mathcal{U}^1_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[ \sqrt{g} \, G^{-1}_{\mu, \nu} \Lambda^1_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_U1"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.sqrt_g(e1, e2, e3) * self.Ginv(e1, e2, e3)[:, :, :, m, n],
]
self._U1 = self.create_basis_op(
fun,
"Hcurl",
"Hdiv",
name="U1",
)
return self._U1
@property
def Xv(self):
r"""Basis projection operator
.. math::
\mathcal{X}^v_{(\mu,ijk),(\nu,mno)} := \hat{\Pi}^{0, \mu}_{ijk} \left[ DF_{\mu, \nu}\Lambda^{0, \nu}_{mno} \right] \,.
"""
if not hasattr(self, "_Xv"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.DF(e1, e2, e3)[:, :, :, m, n],
]
self._Xv = self.create_basis_op(
fun,
"H1vec",
"H1vec",
name="Xv",
)
return self._Xv
@property
def X1(self):
r"""Basis projection operator
.. math::
\mathcal{X}^1_{(\mu, ijk),(\nu, mno)} := \hat{\Pi}^{0, \mu}_{ijk} \left[ DF^{-\top}_{\mu, \nu}\Lambda^1_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_X1"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.DFinvT(e1, e2, e3)[:, :, :, m, n],
]
self._X1 = self.create_basis_op(
fun,
"Hcurl",
"H1vec",
name="X1",
)
return self._X1
@property
def X2(self):
r"""Basis projection operator
.. math::
\mathcal{X}^2_{(\mu, ijk),(\nu, mno)} := \hat{\Pi}^{0, \mu}_{ijk} \left[ \frac{DF_{\mu, \nu}}{\sqrt{g}} \Lambda^2_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_X2"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: self.DF(e1, e2, e3)[:, :, :, m, n] / self.sqrt_g(e1, e2, e3),
]
self._X2 = self.create_basis_op(
fun,
"Hdiv",
"H1vec",
name="X2",
)
return self._X2
@property
def W1(self):
r"""Basis projection operator
.. math::
\mathcal{W}^1_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^1_{(\mu, ijk)} \left[\frac{\hat{\rho}^3_{\text{eq}}}{\sqrt{g}}\Lambda^1_{(\nu, mno)} \right] \,.
"""
if not hasattr(self, "_W1"):
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: (
self.weights["eq_mhd"].n3(
e1,
e2,
e3,
)
/ self.sqrt_g(e1, e2, e3)
if m == n
else 0 * e1
),
]
self._W1 = self.create_basis_op(
fun,
"Hcurl",
"Hcurl",
name="W1",
)
return self._W1
@property
def R1(self):
r"""Basis projection operator
.. math::
\mathcal{R}^1_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^1_{(\mu, ijk)} \left[\frac{\mathcal R^J_{\mu, \nu}}{\sqrt{g}}\Lambda^2_{(\nu, mno)} \right] \,.
with the rotation matrix
.. math::
\mathcal R^J_{\mu, \nu} := \epsilon_{\mu \alpha \nu}\, J^2_{\textnormal{eq}, \alpha}\,,\qquad s.t. \qquad \mathcal R^J \vec v = \vec J^2_{\textnormal{eq}} \times \vec v\,,
where :math:`\epsilon_{\mu \alpha \nu}` stands for the Levi-Civita tensor and :math:`J^2_{\textnormal{eq}, \alpha}` is the :math:`\alpha`-component of the MHD equilibrium current density (2-form).
"""
if not hasattr(self, "_R1"):
rot_J = LocalRotationMatrix(
self.weights["eq_mhd"].j2_1,
self.weights["eq_mhd"].j2_2,
self.weights["eq_mhd"].j2_3,
)
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: rot_J(e1, e2, e3)[:, :, :, m, n] / self.sqrt_g(e1, e2, e3),
]
self._R1 = self.create_basis_op(
fun,
"Hdiv",
"Hcurl",
name="R1",
)
return self._R1
@property
def R2(self):
r"""Basis projection operator
.. math::
\mathcal{R}^2_{(\mu, ijk), (\nu, mno)} := \hat{\Pi}^2_{(\mu, ijk)} \left[\mathcal R^J_{\mu, \beta} G^{-1}_{\beta, \nu} \Lambda^2_{(\nu, mno)} \right] \,.
with the rotation matrix
.. math::
\mathcal R^J_{\mu, \beta} := \epsilon_{\mu \alpha \beta}\, J^2_{\textnormal{eq}, \alpha}\,,\qquad s.t. \qquad \mathcal R^J \vec v = \vec J^2_{\textnormal{eq}} \times \vec v\,,
where :math:`\epsilon_{\mu \alpha \beta}` stands for the Levi-Civita tensor and :math:`J^2_{\textnormal{eq}, \alpha}` is the :math:`\alpha`-component of the MHD equilibrium current density (2-form).
"""
if not hasattr(self, "_R2"):
rot_J = LocalRotationMatrix(
self.weights["eq_mhd"].j2_1,
self.weights["eq_mhd"].j2_2,
self.weights["eq_mhd"].j2_3,
)
fun = []
for m in range(3):
fun += [[]]
for n in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m, n=n: (self.Ginv(e1, e2, e3) @ rot_J(e1, e2, e3))[:, :, :, m, n],
]
self._R2 = self.create_basis_op(
fun,
"Hdiv",
"Hdiv",
name="R2",
)
return self._R2
@property
def PB(self):
r"""
Basis projection operator
.. math::
\mathcal P^b_{ijk, (\mu, mno)} := \hat \Pi^0_{ijk} \left[\frac{1}{\sqrt g} \hat{b}^1_{\text{eq},\mu} \cdot \Lambda^2_{\mu, mno}\right]\,.
"""
if not hasattr(self, "_PB"):
fun = [[]]
for m in range(3):
fun[-1] += [
lambda e1, e2, e3, m=m: self.weights["eq_mhd"].unit_b1(e1, e2, e3)[m] / self.sqrt_g(e1, e2, e3),
]
self._PB = self.create_basis_op(
fun,
"Hdiv",
"H1",
name="PB",
)
return self._PB
##########################################
# Wrapper around BasisProjectionOperator #
##########################################
[docs]
def create_basis_op(
self,
fun: list,
V_id: str,
W_id: str,
assemble: bool = False,
name: str = None,
):
r"""Basis projection operator :math:`V^\alpha_h \to V^\beta_h` with given (rank 0, 1 or 2) weight function :math:`A(\boldsymbol \eta)`:
.. math::
\mathcal P_{(\mu, ijk),(\nu, mno)} = \hat \Pi^\beta_{\mu, ijk} \left( A_{\mu,\nu}\,\Lambda^\alpha_{\nu, mno} \right)\,.
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
----------
fun : list[list[callable | ndarray]]
2d list of either all 3d arrays or all scalar functions of eta1, eta2, eta3 (must allow matrix evaluations).
3d arrays must have shape corresponding to the 1d quad_grids of V1-VectorFemSpace.
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').
assemble: bool
Whether to assemble the DOF matrix.
name: bstr
Name of the operator.
Returns
-------
out : A BasisProjectionOperator object.
"""
assert isinstance(fun, list)
if W_id in {"H1", "L2"}:
assert len(fun) == 1
else:
assert len(fun) == 3
for row in fun:
assert isinstance(row, list)
if V_id in {"H1", "L2"}:
assert len(row) == 1
else:
assert len(row) == 3
if self.derham.with_local_projectors:
out = BasisProjectionOperatorLocal(
self.derham.projectors[W_id],
self.derham.fem_spaces[V_id],
fun,
self.derham.extraction_ops[V_id],
self.derham.boundary_ops[V_id],
self.derham.extraction_ops[W_id],
self.derham.boundary_ops[W_id],
transposed=False,
)
else:
out = BasisProjectionOperator(
self.derham.projectors[W_id],
self.derham.fem_spaces[V_id],
fun,
V_extraction_op=self.derham.extraction_ops[V_id],
V_boundary_op=self.derham.boundary_ops[V_id],
transposed=False,
polar_shift=self.domain.pole,
)
if assemble:
logger.debug(f'\nAssembling BasisProjectionOperator "{name}" with V={V_id}, W={W_id}.')
out.assemble()
logger.debug("Done.")
return out
[docs]
class BasisProjectionOperatorLocal(LinOpWithTransp):
r"""
Class for assembling basis projection operators in 3d, based on local projectors.
A basis projection operator :math:`\mathcal P: \mathbb R^{N_\alpha} \to \mathbb R^{N_\beta}` is defined by the matrix
.. math::
\mathcal P_{(\mu, ijk),(\nu, mno)} = \hat \Pi^\beta_{\mu, ijk} \left( A_{\mu,\nu}\,\Lambda^\alpha_{\nu, mno} \right)\,,
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`. The operator :math:`\hat \Pi^\beta: V^\beta \to \mathbb R^{N_\beta}`
is a local commuting projector from the continuous space
into the space of coefficients.
Finally, extraction and boundary operators can be applied to the basis projection operator matrix, :math:`B_P * E_P * \mathcal P * E_V^T * B_V^T`.
Parameters
----------
P : struphy.feec.projectors.CommutingProjectorLocal
Local commuting projector mapping into TensorFemSpace/VectorFemSpace W = P.space (codomain of operator).
V : feectools.fem.basic.FemSpace
Finite element spline space (domain, input space).
weights : list
Weight function(s) (callables) in a 2d list of shape corresponding to number of components of domain/codomain.
V_extraction_op : PolarExtractionOperator | IdentityOperator
Extraction operator to polar sub-space of V.
V_boundary_op : BoundaryOperator | IdentityOperator
Boundary operator that sets essential boundary conditions on V.
P_extraction_op : PolarExtractionOperator | IdentityOperator
Extraction operator to polar sub-space of the domain of P.
P_boundary_op : BoundaryOperator | IdentityOperator
Boundary operator that sets essential boundary conditions on the domain of P.
transposed : bool
Whether to assemble the transposed operator.
"""
def __init__(
self,
P: CommutingProjectorLocal,
V: FemSpace,
weights: list,
V_extraction_op: PolarExtractionOperator | IdentityOperator = None,
V_boundary_op: BoundaryOperator | IdentityOperator = None,
P_extraction_op: PolarExtractionOperator | IdentityOperator = None,
P_boundary_op: BoundaryOperator | IdentityOperator = None,
transposed: bool = False,
):
# only for M1 Mac users
PSYDAC_BACKEND_GPYCCEL["flags"] = "-O3 -march=native -mtune=native -ffast-math -ffree-line-length-none"
self._P = P
self._V = V
# set extraction operators
if P_extraction_op is not None:
self._P_extraction_op = P_extraction_op
else:
self._P_extraction_op = IdentityOperator(P.coeff_space)
if V_extraction_op is not None:
self._V_extraction_op = V_extraction_op
else:
self._V_extraction_op = IdentityOperator(V.coeff_space)
# set boundary operators
if P_boundary_op is not None:
self._P_boundary_op = P_boundary_op
else:
self._P_boundary_op = IdentityOperator(
self._P_extraction_op.domain,
)
if V_boundary_op is not None:
self._V_boundary_op = V_boundary_op
else:
self._V_boundary_op = IdentityOperator(
self._V_extraction_op.domain,
)
self._weights = weights
self._transposed = transposed
self._dtype = V.coeff_space.dtype
# set domain and codomain symbolic names
self._P_name = self._P.space_id
if hasattr(V.symbolic_space, "name"):
self._V_name = V.symbolic_space.name
elif isinstance(V.symbolic_space, str):
self._V_name = V.symbolic_space
else:
self._V_name = "H1vec"
if transposed:
self._domain_symbolic_name = self._P_name
self._codomain_symbolic_name = self._V_name
else:
self._domain_symbolic_name = self._V_name
self._codomain_symbolic_name = self._P_name
# Are both space scalar spaces : useful to know if _mat will be Stencil or Block Matrix
self._is_scalar = True
if not isinstance(V, TensorFemSpace):
self._is_scalar = False
self._mpi_comm = V.coeff_space.spaces[0].cart.comm
else:
self._mpi_comm = V.coeff_space.cart.comm
if not isinstance(P.fem_space, TensorFemSpace):
self._is_scalar = False
# input space: 3d StencilVectorSpaces and 1d SplineSpaces of each component
if isinstance(V, TensorFemSpace):
self._Vspaces = [V.coeff_space]
self._V1ds = [V.spaces]
self._VNbasis = xp.array([self._V1ds[0][0].nbasis, self._V1ds[0][1].nbasis, self._V1ds[0][2].nbasis])
else:
self._Vspaces = V.coeff_space
self._V1ds = [comp.spaces for comp in V.spaces]
self._VNbasis = xp.array(
[
[self._V1ds[0][0].nbasis, self._V1ds[0][1].nbasis, self._V1ds[0][2].nbasis],
[
self._V1ds[1][0].nbasis,
self._V1ds[1][1].nbasis,
self._V1ds[1][2].nbasis,
],
[self._V1ds[2][0].nbasis, self._V1ds[2][1].nbasis, self._V1ds[2][2].nbasis],
],
)
# output space: 3d StencilVectorSpaces and 1d SplineSpaces of each component
if isinstance(P.fem_space, TensorFemSpace):
self._Wspaces = [P.fem_space.coeff_space]
self._W1ds = [P.fem_space.spaces]
self._periodic = P._periodic
else:
self._Wspaces = P.fem_space.coeff_space
self._W1ds = [comp.spaces for comp in P.fem_space.spaces]
self._periodic = P._periodic[0]
# We get the starts and ends of the Projector. They are the same as the starts and end for the rows of the StencilMatrix or BlockLinearOperator
self._starts = self._P._starts
self._ends = self._P._ends
self._pds = self._P._pds
# Degree of the B-splines
self._degree = self._P._degree
# ============= create and assemble the Basis Projection Operator matrix =======
if self._is_scalar:
self._mat = StencilMatrix(V.coeff_space, P.fem_space.coeff_space)
else:
self._mat = BlockLinearOperator(
V.coeff_space,
P.fem_space.coeff_space,
)
self._mat = self.assemble()
# ========================================================
# build the transposed matrix and applied extraction and boundary operators
if transposed:
self._mat_T = self._mat.T
self._operator = (
self._V_boundary_op
@ self._V_extraction_op
@ self._mat_T
@ self._P_extraction_op.T
@ self._P_boundary_op.T
)
else:
self._operator = (
self._P_boundary_op
@ self._P_extraction_op
@ self._mat
@ self._V_extraction_op.T
@ self._V_boundary_op.T
)
# set domain and codomain
if transposed:
self._domain = self._P.coeff_space
self._codomain = self._V.coeff_space
else:
self._domain = self._V.coeff_space
self._codomain = self._P.coeff_space
@property
def domain(self):
"""Domain vector space (input) of the operator."""
return self._domain
@property
def codomain(self):
"""Codomain vector space (input) of the operator."""
return self._codomain
@property
def dtype(self):
"""Datatype of the operator."""
return self._dtype
@property
def tosparse(self):
return self._mat.tosparse()
@property
def toarray(self):
return self._mat.toarray()
@property
def transposed(self):
"""If the transposed operator is in play."""
return self._transposed
[docs]
def dot(self, v, out=None):
"""
Applies the basis projection operator to the FE coefficients v.
Parameters
----------
v : feectools.linalg.basic.Vector
Vector the operator shall be applied to.
out : feectools.linalg.basic.Vector, optional
If given, the output will be written in-place into this vector.
Returns
-------
out : feectools.linalg.basic.Vector
The output (codomain) vector.
"""
assert isinstance(v, Vector)
assert v.space == self.domain
if out is None:
out = self._operator.dot(v)
else:
assert isinstance(out, Vector)
assert out.space == self.codomain
self._operator.dot(v, out=out)
return out
[docs]
def transpose(self):
"""
Returns the transposed operator.
"""
return BasisProjectionOperatorLocal(
self._P,
self._V,
self._weights,
self._V_extraction_op,
self._V_boundary_op,
self._P_extraction_op,
self._P_boundary_op,
not self.transposed,
)
[docs]
def update_weights(self, weights):
"""Updates self.weights and computes new BasisProjectionOperatorLocal matrix.
Parameters
----------
weights : list
Weight function(s) (callables) in a 2d list of shape corresponding to number of components of domain/codomain.
"""
self._weights = weights
# assemble tensor-product dof matrix
self._mat = self.assemble()
# only need to update the transposed in case where it's needed
if self._transposed:
self._mat_T = self._mat.T
[docs]
def assemble(self):
"""
Assembles the BasisProjectionOperatorLocal. And
store it in self._mat.
"""
# get the needed data :
V = self._V
P = self._P
weights = self._weights
# We determine where we have B-splines and where D-splines.
if self._V_name == "H1":
BoD = ["B", "B", "B"]
elif self._V_name == "Hcurl":
BoD = [["D", "B", "B"], ["B", "D", "B"], ["B", "B", "D"]]
elif self._V_name == "Hdiv":
BoD = [["B", "D", "D"], ["D", "B", "D"], ["D", "D", "B"]]
elif self._V_name == "L2":
BoD = ["D", "D", "D"]
elif self._V_name in ("H1H1H1", "H1vec"):
BoD = [["B", "B", "B"], ["B", "B", "B"], ["B", "B", "B"]]
else:
raise Exception("The FE space name for the input space must be H1, Hcurl, Hdiv, L2 or H1H1H1 or H1vec.")
if isinstance(self._mat, StencilMatrix):
# We get the B and D spline indices this MPI rank must compute
eval_indices_B = P._Basis_functions_indices_B
eval_indices_D = P._Basis_functions_indices_D
if self._V_name == "H1":
eval_indices = eval_indices_B
elif self._V_name == "L2":
eval_indices = eval_indices_D
# We only use this counter to know if we are calling the Projection for the very first time
counter = 0
for col0 in eval_indices[0]:
for col1 in eval_indices[1]:
for col2 in eval_indices[2]:
if counter == 0:
coeff, weigths_dof = P(
weights[0][0],
weighted=True,
B_or_D=BoD,
basis_indices=[col0, col1, col2],
first_go=True,
)
else:
coeff = P(
weights[0][0],
weighted=True,
B_or_D=BoD,
basis_indices=[
col0,
col1,
col2,
],
first_go=False,
pre_computed_dofs=weigths_dof,
)
counter += 1
assemble_basis_projection_operator_local(
self._starts,
self._ends,
self._pds,
self._periodic,
self._degree,
xp.array([col0, col1, col2]),
self._VNbasis,
self._mat._data,
coeff,
P._rows_B_or_D_splines_0[BoD[0]][P._translation_indices_B_or_D_splines_0[BoD[0]][col0]],
P._rows_B_or_D_splines_1[BoD[1]][P._translation_indices_B_or_D_splines_1[BoD[1]][col1]],
P._rows_B_or_D_splines_2[BoD[2]][P._translation_indices_B_or_D_splines_2[BoD[2]][col2]],
P._rowe_B_or_D_splines_0[BoD[0]][P._translation_indices_B_or_D_splines_0[BoD[0]][col0]],
P._rowe_B_or_D_splines_1[BoD[1]][P._translation_indices_B_or_D_splines_1[BoD[1]][col1]],
P._rowe_B_or_D_splines_2[BoD[2]][P._translation_indices_B_or_D_splines_2[BoD[2]][col2]],
)
elif self._P_name == "H1" or self._P_name == "L2":
# We get the B and D spline indices this MPI rank must compute
eval_indices_B = P._Basis_functions_indices_B
eval_indices_D = P._Basis_functions_indices_D
if self._V_name == "Hcurl":
eval_block_0 = [eval_indices_D[0], eval_indices_B[1], eval_indices_B[2]]
eval_block_1 = [eval_indices_B[0], eval_indices_D[1], eval_indices_B[2]]
eval_block_2 = [eval_indices_B[0], eval_indices_B[1], eval_indices_D[2]]
elif self._V_name == "Hdiv":
eval_block_0 = [eval_indices_B[0], eval_indices_D[1], eval_indices_D[2]]
eval_block_1 = [eval_indices_D[0], eval_indices_B[1], eval_indices_D[2]]
eval_block_2 = [eval_indices_D[0], eval_indices_D[1], eval_indices_B[2]]
elif self._V_name in ("H1H1H1", "H1vec"):
eval_block_0 = [eval_indices_B[0], eval_indices_B[1], eval_indices_B[2]]
eval_block_1 = [eval_indices_B[0], eval_indices_B[1], eval_indices_B[2]]
eval_block_2 = [eval_indices_B[0], eval_indices_B[1], eval_indices_B[2]]
else:
raise Exception("The input space name is not defined.")
eval_blocks = [eval_block_0, eval_block_1, eval_block_2]
# Filling the hh-th block
for hh in range(3):
Aux = StencilMatrix(self._Vspaces[hh], self._Wspaces[0])
counter = 0
for col0 in eval_blocks[hh][0]:
for col1 in eval_blocks[hh][1]:
for col2 in eval_blocks[hh][2]:
if counter == 0:
coeff, weigths_dof = P(
weights[0][hh],
weighted=True,
B_or_D=BoD[hh],
basis_indices=[
col0,
col1,
col2,
],
first_go=True,
)
else:
coeff = P(
weights[0][hh],
weighted=True,
B_or_D=BoD[hh],
basis_indices=[
col0,
col1,
col2,
],
first_go=False,
pre_computed_dofs=weigths_dof,
)
counter += 1
assemble_basis_projection_operator_local(
self._starts,
self._ends,
self._pds,
self._periodic,
self._degree,
xp.array(
[
col0,
col1,
col2,
],
),
self._VNbasis[hh],
Aux._data,
coeff,
P._rows_B_or_D_splines_0[BoD[hh][0]][
P._translation_indices_B_or_D_splines_0[BoD[hh][0]][col0]
],
P._rows_B_or_D_splines_1[BoD[hh][1]][
P._translation_indices_B_or_D_splines_1[BoD[hh][1]][col1]
],
P._rows_B_or_D_splines_2[BoD[hh][2]][
P._translation_indices_B_or_D_splines_2[BoD[hh][2]][col2]
],
P._rowe_B_or_D_splines_0[BoD[hh][0]][
P._translation_indices_B_or_D_splines_0[BoD[hh][0]][col0]
],
P._rowe_B_or_D_splines_1[BoD[hh][1]][
P._translation_indices_B_or_D_splines_1[BoD[hh][1]][col1]
],
P._rowe_B_or_D_splines_2[BoD[hh][2]][
P._translation_indices_B_or_D_splines_2[BoD[hh][2]][col2]
],
)
self._mat[(0, hh)] = Aux
elif self._V_name == "H1" or self._V_name == "L2":
# We get the B and D spline indices this MPI rank must compute
eval_indices_B = P._Basis_function_indices_mark_B
eval_indices_D = P._Basis_function_indices_mark_D
if self._V_name == "H1":
eval_indices = eval_indices_B
elif self._V_name == "L2":
eval_indices = eval_indices_D
Aux0 = StencilMatrix(self._Vspaces[0], self._Wspaces[0])
Aux1 = StencilMatrix(self._Vspaces[0], self._Wspaces[1])
Aux2 = StencilMatrix(self._Vspaces[0], self._Wspaces[2])
Aux = [Aux0, Aux1, Aux2]
counter = 0
for col0 in eval_indices[0]:
for col1 in eval_indices[1]:
for col2 in eval_indices[2]:
if counter == 0:
coeff, weigths_dof = P(
[weights[0][0], weights[1][0], weights[2][0]],
weighted=True,
B_or_D=BoD,
basis_indices=[
col0,
col1,
col2,
],
first_go=True,
)
else:
coeff = P(
[weights[0][0], weights[1][0], weights[2][0]],
weighted=True,
B_or_D=BoD,
basis_indices=[col0, col1, col2],
first_go=False,
pre_computed_dofs=weigths_dof,
)
counter += 1
for h in range(3):
assemble_basis_projection_operator_local(
self._starts[h],
self._ends[h],
self._pds[h],
self._periodic,
self._degree,
xp.array(
[
col0,
col1,
col2,
],
),
self._VNbasis,
Aux[h]._data,
coeff[h],
P._rows_block_B_or_D_splines[0][h][BoD[0]][
P._translation_indices_block_B_or_D_splines[0][h][BoD[0]][col0]
],
P._rows_block_B_or_D_splines[1][h][BoD[1]][
P._translation_indices_block_B_or_D_splines[1][h][BoD[1]][col1]
],
P._rows_block_B_or_D_splines[2][h][BoD[2]][
P._translation_indices_block_B_or_D_splines[2][h][BoD[2]][col2]
],
P._rowe_block_B_or_D_splines[0][h][BoD[0]][
P._translation_indices_block_B_or_D_splines[0][h][BoD[0]][col0]
],
P._rowe_block_B_or_D_splines[1][h][BoD[1]][
P._translation_indices_block_B_or_D_splines[1][h][BoD[1]][col1]
],
P._rowe_block_B_or_D_splines[2][h][BoD[2]][
P._translation_indices_block_B_or_D_splines[2][h][BoD[2]][col2]
],
)
for h in range(3):
self._mat[(h, 0)] = Aux[h]
else:
# We get the B and D spline indices this MPI rank must compute
eval_indices_B = P._Basis_function_indices_mark_B
eval_indices_D = P._Basis_function_indices_mark_D
if self._V_name == "Hcurl":
eval_block_0 = [eval_indices_D[0], eval_indices_B[1], eval_indices_B[2]]
eval_block_1 = [eval_indices_B[0], eval_indices_D[1], eval_indices_B[2]]
eval_block_2 = [eval_indices_B[0], eval_indices_B[1], eval_indices_D[2]]
elif self._V_name == "Hdiv":
eval_block_0 = [eval_indices_B[0], eval_indices_D[1], eval_indices_D[2]]
eval_block_1 = [eval_indices_D[0], eval_indices_B[1], eval_indices_D[2]]
eval_block_2 = [eval_indices_D[0], eval_indices_D[1], eval_indices_B[2]]
elif self._V_name in ("H1H1H1", "H1vec"):
eval_block_0 = [eval_indices_B[0], eval_indices_B[1], eval_indices_B[2]]
eval_block_1 = [eval_indices_B[0], eval_indices_B[1], eval_indices_B[2]]
eval_block_2 = [eval_indices_B[0], eval_indices_B[1], eval_indices_B[2]]
else:
raise Exception("The input space name is not defined.")
eval_blocks = [eval_block_0, eval_block_1, eval_block_2]
# Iterates over the input block entries
for hh in range(3):
Aux0 = StencilMatrix(self._Vspaces[hh], self._Wspaces[0])
Aux1 = StencilMatrix(self._Vspaces[hh], self._Wspaces[1])
Aux2 = StencilMatrix(self._Vspaces[hh], self._Wspaces[2])
Aux = [Aux0, Aux1, Aux2]
counter = 0
for col0 in eval_blocks[hh][0]:
for col1 in eval_blocks[hh][1]:
for col2 in eval_blocks[hh][2]:
if counter == 0:
coeff, weigths_dof = P(
[weights[0][hh], weights[1][hh], weights[2][hh]],
weighted=True,
B_or_D=BoD[hh],
basis_indices=[
col0,
col1,
col2,
],
first_go=True,
)
else:
coeff = P(
[weights[0][hh], weights[1][hh], weights[2][hh]],
weighted=True,
B_or_D=BoD[hh],
basis_indices=[
col0,
col1,
col2,
],
first_go=False,
pre_computed_dofs=weigths_dof,
)
counter += 1
# Iterates over the output block entries
for h in range(3):
assemble_basis_projection_operator_local(
self._starts[h],
self._ends[h],
self._pds[h],
self._periodic,
self._degree,
xp.array(
[
col0,
col1,
col2,
],
),
self._VNbasis[hh],
Aux[h]._data,
coeff[h],
P._rows_block_B_or_D_splines[0][h][BoD[hh][0]][
P._translation_indices_block_B_or_D_splines[0][h][BoD[hh][0]][col0]
],
P._rows_block_B_or_D_splines[1][h][BoD[hh][1]][
P._translation_indices_block_B_or_D_splines[1][h][BoD[hh][1]][col1]
],
P._rows_block_B_or_D_splines[2][h][BoD[hh][2]][
P._translation_indices_block_B_or_D_splines[2][h][BoD[hh][2]][col2]
],
P._rowe_block_B_or_D_splines[0][h][BoD[hh][0]][
P._translation_indices_block_B_or_D_splines[0][h][BoD[hh][0]][col0]
],
P._rowe_block_B_or_D_splines[1][h][BoD[hh][1]][
P._translation_indices_block_B_or_D_splines[1][h][BoD[hh][1]][col1]
],
P._rowe_block_B_or_D_splines[2][h][BoD[hh][2]][
P._translation_indices_block_B_or_D_splines[2][h][BoD[hh][2]][col2]
],
)
for h in range(3):
self._mat[(h, hh)] = Aux[h]
self._mat.update_ghost_regions()
return self._mat
[docs]
@auto_convert_docstring
class BasisProjectionOperator(LinOpWithTransp):
r"""
Class for assembling basis projection operators in 3d.
A basis projection operator :math:`\mathcal P: \mathbb R^{N_\alpha} \to \mathbb R^{N_\beta}` is defined by the matrix
.. math::
\mathcal{P}_{(\mu, ijk),(\nu, mno)} = \hat{\Pi}^\beta_{\mu, ijk} \left( A_{\mu,\nu}\,\Lambda^{\alpha}_{\nu, mno} \right)\,,
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`. The operator :math:`\hat \Pi^\beta: V^\beta \to \mathbb R^{N_\beta}`
is a commuting projector from the continuous space
into the space of coefficients; it can be decomposed into computation of degrees of freedom (DOFs)
:math:`\sigma^\beta: V^\beta \to \mathbb R^{N_\beta}` and inversion of the inter/-histopolation matrix
:math:`\mathcal (I^\beta)^{-1}: \mathbb R^{N_\beta} \to \mathbb R^{N_\beta}`:
.. math::
\hat \Pi^\beta = (I^\beta)^{-1} \sigma^\beta\,.
:math:`I^\beta` is usually a Kronecker product and thus fast to invert; this inversion is performed when calling the dot-product
of the ``BasisProjectionOperator``. The DOFs are precomputed and stored in StencilVector
format, because the local support of each :math:`\Lambda^\alpha_{\nu, mno}`.
Finally, extraction and boundary operators can be applied to the DOFs, :math:`B_P * P * \sigma * E_V^T * B_V^T`.
Parameters
----------
P : struphy.feec.projectors.Projector
Global commuting projector mapping into TensorFemSpace/VectorFemSpace W = P.space (codomain of operator).
V : feectools.fem.basic.FemSpace
Finite element spline space (domain, input space).
weights : list
Weight function(s) (callables or xp.ndarrays) in a 2d list of shape corresponding to number of components of domain/codomain.
V_extraction_op : PolarExtractionOperator | IdentityOperator
Extraction operator to polar sub-space of V.
V_boundary_op : BoundaryOperator | IdentityOperator
Boundary operator that sets essential boundary conditions on V.
P_extraction_op : PolarExtractionOperator | IdentityOperator
Extraction operator to polar sub-space of the domain of P.
P_boundary_op : BoundaryOperator | IdentityOperator
Boundary operator that sets essential boundary conditions on the domain of P.
transposed : bool
Whether to assemble the transposed operator.
polar_shift : bool
Whether there are metric coefficients contained in "weights" which are singular at eta1=0. If True, interpolation points at eta1=0 are shifted away from the singularity by 1e-5.
use_cache : bool
Whether to store some information computed in self.assemble for reuse. Set it to true if planned to update the weights later.
"""
def __init__(
self,
P: CommutingProjector,
V: FemSpace,
weights: list,
*,
V_extraction_op: PolarExtractionOperator | IdentityOperator = None,
V_boundary_op: BoundaryOperator | IdentityOperator = None,
P_extraction_op: PolarExtractionOperator | IdentityOperator = None,
P_boundary_op: BoundaryOperator | IdentityOperator = None,
transposed: bool = False,
polar_shift: bool = False,
use_cache: bool = False,
):
# only for M1 Mac users
PSYDAC_BACKEND_GPYCCEL["flags"] = "-O3 -march=native -mtune=native -ffast-math -ffree-line-length-none"
self._P = P
self._V = V
# set extraction operators
if P_extraction_op is not None:
self._P_extraction_op = P_extraction_op
else:
self._P_extraction_op = P.dofs_extraction_op
if V_extraction_op is not None:
self._V_extraction_op = V_extraction_op
else:
self._V_extraction_op = IdentityOperator(V.coeff_space)
# set boundary operators
if P_boundary_op is not None:
self._P_boundary_op = P_boundary_op
else:
self._P_boundary_op = P.boundary_op
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,
)
self._weights = weights
self._transposed = transposed
self._polar_shift = polar_shift
self._dtype = V.coeff_space.dtype
self._use_cache = use_cache
# Create cache
if use_cache:
self._cache = {}
# set domain and codomain symbolic names
if hasattr(P.space.symbolic_space, "name"):
P_name = P.space.symbolic_space.name
elif isinstance(P.space.symbolic_space, str):
P_name = P.space.symbolic_space
else:
P_name = "H1vec"
if hasattr(V.symbolic_space, "name"):
V_name = V.symbolic_space.name
elif isinstance(V.symbolic_space, str):
V_name = V.symbolic_space
else:
V_name = "H1vec"
if transposed:
self._domain_symbolic_name = P_name
self._codomain_symbolic_name = V_name
else:
self._domain_symbolic_name = V_name
self._codomain_symbolic_name = P_name
# Are both space scalar spaces : useful to know if _dof_mat will be Stencil or Block Matrix
self._is_scalar = True
if not isinstance(V, TensorFemSpace):
self._is_scalar = False
self._mpi_comm = V.coeff_space.spaces[0].cart.comm
else:
self._mpi_comm = V.coeff_space.cart.comm
if not isinstance(P.space, TensorFemSpace):
self._is_scalar = False
# ============= create and assemble tensor-product dof matrix =======
if self._is_scalar:
self._dof_mat = StencilMatrix(V.coeff_space, P.space.coeff_space)
else:
self._dof_mat = BlockLinearOperator(
V.coeff_space,
P.space.coeff_space,
)
self._dof_mat = self.assemble()
# ========================================================
# build composed linear operator BP * P * DOF * EV^T * BV^T or transposed
if transposed:
self._dof_mat_T = self._dof_mat.T
self._dof_operator = (
self._V_boundary_op
@ self._V_extraction_op
@ self._dof_mat_T
@ self._P_extraction_op.T
@ self._P_boundary_op.T
)
self._x0 = self._dof_operator.domain.zeros()
else:
self._dof_operator = (
self._P_boundary_op
@ self._P_extraction_op
@ self._dof_mat
@ self._V_extraction_op.T
@ self._V_boundary_op.T
)
self._x0 = self._dof_operator.codomain.zeros()
# set domain and codomain
self._domain = self.dof_operator.domain
self._codomain = self.dof_operator.codomain
# temporary vectors for dot product
self._tmp_dom = self._dof_operator.domain.zeros()
self._tmp_codom = self._dof_operator.codomain.zeros()
@property
def domain(self):
"""Domain vector space (input) of the operator."""
return self._domain
@property
def codomain(self):
"""Codomain vector space (input) of the operator."""
return self._codomain
@property
def dtype(self):
"""Datatype of the operator."""
return self._dtype
@property
def tosparse(self):
raise NotImplementedError()
@property
def toarray(self):
raise NotImplementedError()
@property
def transposed(self):
"""If the transposed operator is in play."""
return self._transposed
@property
def dof_operator(self):
"""The degrees of freedom operator as composite linear operator containing polar extraction and boundary operators."""
return self._dof_operator
[docs]
def dot(self, v, out=None, tol=1e-14, maxiter=1000):
"""
Applies the basis projection operator to the FE coefficients v.
Parameters
----------
v : feectools.linalg.basic.Vector
Vector the operator shall be applied to.
out : feectools.linalg.basic.Vector, optional
If given, the output will be written in-place into this vector.
tol : float, optional
Stop tolerance in iterative solve (only used in polar case).
maxiter : int, optional
Maximum number of iterations in iterative solve (only used in polar case).
Returns
-------
out : feectools.linalg.basic.Vector
The output (codomain) vector.
"""
assert isinstance(v, Vector)
assert v.space == self.domain
if out is None:
out = self.codomain.zeros()
if self.transposed:
# 1. apply inverse transposed inter-/histopolation matrix, 2. apply transposed dof operator
out = self.dof_operator.dot(
self._P.solve(
v,
True,
apply_bc=True,
),
)
else:
# 1. apply dof operator, 2. apply inverse inter-/histopolation matrix
out = self._P.solve(
self.dof_operator.dot(
v,
),
False,
apply_bc=True,
)
assert isinstance(out, Vector)
assert out.space == self.codomain
if self.transposed:
# 1. apply inverse transposed inter-/histopolation matrix, 2. apply transposed dof operator
self._P.solve(v, True, apply_bc=True, out=self._tmp_dom, x0=self._x0)
self._tmp_dom.copy(out=self._x0)
self.dof_operator.dot(self._tmp_dom, out=out)
else:
# 1. apply dof operator, 2. apply inverse inter-/histopolation matrix
self.dof_operator.dot(v, out=self._tmp_codom)
self._P.solve(self._tmp_codom, False, apply_bc=True, out=out, x0=self._x0)
out.copy(out=self._x0)
return out
[docs]
def transpose(self, conjugate=False):
"""
Returns the transposed operator.
"""
return BasisProjectionOperator(
self._P,
self._V,
self._weights,
V_extraction_op=self._V_extraction_op,
V_boundary_op=self._V_boundary_op,
P_extraction_op=self._P_extraction_op,
P_boundary_op=self._P_boundary_op,
transposed=not self.transposed,
polar_shift=self._polar_shift,
use_cache=self._use_cache,
)
[docs]
def update_weights(self, weights):
"""Updates self.weights and computes new DOF matrix.
Parameters
----------
weights : list
Weight function(s) (callables or xp.ndarrays) in a 2d list of shape corresponding to number of components of domain/codomain.
"""
self._weights = weights
# assemble tensor-product dof matrix
self._dof_mat = self.assemble()
# only need to update the transposed in case where it's needed
# (no need to recreate a new ComposedOperator)
if self._transposed:
self._dof_mat_T = self._dof_mat.transpose(out=self._dof_mat_T)
[docs]
def assemble(self, weights=None):
"""
Assembles the tensor-product DOF matrix sigma_i(weights[i,j]*Lambda_j), where i=(i1, i2, ...)
and j=(j1, j2, ...) depending on the number of spatial dimensions (1d, 2d or 3d). And
store it in self._dof_mat.
"""
rank = MPI.COMM_WORLD.Get_rank()
# get the needed data :
V = self._V
P = self._P.projector_tensor
if weights is None:
weights = self._weights
else:
assert isinstance(weights, list)
assert isinstance(weights[0], list)
polar_shift = self._polar_shift
# input space: 3d StencilVectorSpaces and 1d SplineSpaces of each component
if isinstance(V, TensorFemSpace):
_Vspaces = [V.coeff_space]
_V1ds = [V.spaces]
else:
_Vspaces = V.coeff_space
_V1ds = [comp.spaces for comp in V.spaces]
# output space: 3d StencilVectorSpaces and 1d SplineSpaces of each component
if isinstance(P.space, TensorFemSpace):
_Wspaces = [P.space.coeff_space]
_W1ds = [P.space.spaces]
else:
_Wspaces = P.space.coeff_space
_W1ds = [comp.spaces for comp in P.space.spaces]
# retrieve number of quadrature points of each component (=1 for interpolation)
_nqs = [[P.grid_x[comp][direction].shape[1] for direction in range(V.ldim)] for comp in range(len(_W1ds))]
# ouptut vector space (codomain), row of block
for i, (Wspace, W1d, nq, weight_line) in enumerate(zip(_Wspaces, _W1ds, _nqs, weights)):
_Wdegrees = [space.degree for space in W1d]
# input vector space (domain), column of block
for j, (Vspace, V1d, loc_weight) in enumerate(zip(_Vspaces, _V1ds, weight_line)):
_starts_in = xp.array(Vspace.starts)
_ends_in = xp.array(Vspace.ends)
_pads_in = xp.array(Vspace.pads)
_starts_out = xp.array(Wspace.starts)
_ends_out = xp.array(Wspace.ends)
_pads_out = xp.array(Wspace.pads)
# use cached information if asked
if self._use_cache:
if (i, j) in self._cache:
_ptsG, _wtsG, _spans, _bases, _subs = self._cache[
(
i,
j,
)
]
else:
_ptsG, _wtsG, _spans, _bases, _subs = prepare_projection_of_basis(
V1d,
W1d,
_starts_out,
_ends_out,
nq,
polar_shift,
)
self._cache[(i, j)] = (
_ptsG,
_wtsG,
_spans,
_bases,
_subs,
)
else:
# no cache
_ptsG, _wtsG, _spans, _bases, _subs = prepare_projection_of_basis(
V1d,
W1d,
_starts_out,
_ends_out,
nq,
polar_shift,
)
_ptsG = [pts.flatten() for pts in _ptsG]
_Vnbases = [int(space.nbasis) for space in V1d]
_Wnbases = [int(space.nbasis) for space in W1d]
# Evaluate weight function at quadrature points
# evaluate weight at quadrature points
if callable(loc_weight):
PTS = xp.meshgrid(*_ptsG, indexing="ij")
mat_w = loc_weight(*PTS).copy()
elif isinstance(loc_weight, xp.ndarray):
assert loc_weight.shape == (len(_ptsG[0]), len(_ptsG[1]), len(_ptsG[2]))
mat_w = loc_weight
elif loc_weight is not None:
raise TypeError(
"weights must be xp.ndarray, callable or None",
)
# Call the kernel if weight function is not zero or in the scalar case
# to avoid calling _block of a StencilMatrix in the else
not_weight_zero = xp.array(
int(loc_weight is not None and xp.any(xp.abs(mat_w) > 1e-14)),
)
if self._mpi_comm is not None:
self._mpi_comm.Allreduce(
MPI.IN_PLACE,
not_weight_zero,
op=MPI.LOR,
)
if not_weight_zero or self._is_scalar:
# get cell of block matrix (don't instantiate if all zeros)
if self._is_scalar:
dofs_mat = self._dof_mat
else:
dofs_mat = self._dof_mat[i, j]
if dofs_mat is None:
# Maybe in a previous iteration we had more zeros
self._dof_mat[i, j] = StencilMatrix(
Vspace,
Wspace,
backend=PSYDAC_BACKEND_GPYCCEL,
precompiled=True,
)
dofs_mat = self._dof_mat[i, j]
kernel = Pyccelkernel(
getattr(
basis_projection_kernels,
"assemble_dofs_for_weighted_basisfuns_" + str(V.ldim) + "d",
),
)
logger.debug(f"Assemble block {i, j}")
kernel(
dofs_mat._data,
_starts_in,
_ends_in,
_pads_in,
_starts_out,
_ends_out,
_pads_out,
mat_w,
*_wtsG,
*_spans,
*_bases,
*_subs,
*_Vnbases,
*_Wnbases,
*_Wdegrees,
)
dofs_mat.set_backend(
backend=PSYDAC_BACKEND_GPYCCEL,
precompiled=True,
)
dofs_mat.update_ghost_regions()
else:
self._dof_mat[i, j] = None
return self._dof_mat
[docs]
def prepare_projection_of_basis(V1d, W1d, starts_out, ends_out, n_quad=None, polar_shift=False):
"""Obtain knot span indices and basis functions evaluated at projection point sets of a given space.
Parameters
----------
V1d : 3-list
Three SplineSpace objects from Psydac from the input space (to be projected).
W1d : 3-list
Three SplineSpace objects from Psydac from the output space (projected onto).
starts_out : 3-list
Global starting indices of process.
ends_out : 3-list
Global ending indices of process.
n_quad : 3_list
Number of quadrature points per histpolation interval. If not given, is set to V1d.degree + 1.
Returns
-------
ptsG : 3-tuple of 2d float arrays
Quadrature points (or Greville points for interpolation) in each dimension in format (interval, quadrature point).
wtsG : 3-tuple of 2d float arrays
Quadrature weights (or ones for interpolation) in each dimension in format (interval, quadrature point).
spans : 3-tuple of 2d int arrays
Knot span indices in each direction in format (n, nq).
bases : 3-tuple of 3d float arrays
Values of degree + 1 non-zero eta basis functions at quadrature points in format (n, nq, basis).
subs : 3-tuple of 1f int arrays
Sub-interval indices (either 0 or 1). This index is 1 if an element has to be split for exact integration (even spline degree).
"""
pts, wts, subs, spans, bases = [], [], [], [], []
if n_quad is None:
n_quad = [None] * 3
# Loop over direction, prepare point sets and evaluate basis functions
for d, (space_in, space_out, s, e) in enumerate(zip(V1d, W1d, starts_out, ends_out)):
# point sets and weights for inter-/histopolation
pts_i, wts_i, subs_i = get_pts_and_wts(
space_out,
s,
e,
n_quad=n_quad[d],
polar_shift=d == 0 and polar_shift,
)
pts += [pts_i]
wts += [wts_i]
subs += [subs_i]
# Knot span indices and V-basis functions evaluated at W-point sets
s_i, b_i = get_span_and_basis(pts[-1], space_in)
spans += [s_i]
bases += [b_i]
# logger.info("#################################################")
# logger.info("#################################################")
# logger.info("W1d[0]:")
# logger.info(W1d[0])
# logger.info("W1d[1]:")
# logger.info(W1d[1])
# logger.info("W1d[2]:")
# logger.info(W1d[2])
# logger.info("pts :")
# logger.info(pts)
# logger.info("#################################################")
# logger.info("#################################################")
return tuple(pts), tuple(wts), tuple(spans), tuple(bases), tuple(subs)
[docs]
class CoordinateProjector(LinearOperator):
r"""
Class of projectors on one component of a :class:`~feectools.linalg.block.BlockVectorSpace`.
Represent the projection on the :math:`\mu`-th component :
.. math::
\begin{align}
P_\mu : \ & V_1 \times \ldots \times V_\mu \times \ldots \times V_n \longrightarrow V_\mu \,,
\\[2mm]
&\vec{x} = (x_1,\ldots,x_\mu,\ldots ,x_n) \mapsto x_\mu \,.
\end{align}
Parameters
----------
mu : int
The component on which to project.
V : BlockVectorSpace | PolarDerhamSpace
Domain, input space.
Vmu : StencilVectorSpace | PolarDerhamSpace
Codomain, out space, must be :math:`\mu`-th space of V.
"""
def __init__(
self,
mu: int,
V: BlockVectorSpace | PolarDerhamSpace,
Vmu: StencilVectorSpace | PolarDerhamSpace,
):
assert isinstance(mu, int)
if isinstance(V, PolarDerhamSpace):
assert V.parent_space.spaces[mu] == Vmu.parent_space
else:
assert V.spaces[mu] == Vmu
self.dir = mu
self._domain = V
self._codomain = Vmu
self._dtype = Vmu.dtype
@property
def domain(self):
"""Domain vector space (input) of the operator."""
return self._domain
@property
def codomain(self):
"""Codomain vector space (input) of the operator."""
return self._codomain
@property
def dtype(self):
"""Datatype of the operator."""
return self._dtype
@property
def tosparse(self):
raise NotImplementedError()
@property
def toarray(self):
raise NotImplementedError()
[docs]
def transpose(self, conjugate=False):
return CoordinateInclusion(self.dir, self._domain, self._codomain)
[docs]
def dot(
self,
v: BlockVector | PolarVector,
out=None,
):
assert v.space == self._domain
if isinstance(self.domain, PolarDerhamSpace):
if out is not None:
assert out.space == self._codomain
out *= 0.0
else:
out = self.codomain.zeros()
out._tp += v.tp.blocks[self.dir]
else:
if out is not None:
assert out.space == self._codomain
out *= 0.0
out += v.blocks[self.dir]
else:
out = v.blocks[self.dir].copy()
out.update_ghost_regions() # TODO: this is usually not done within .dot, should maybe be removed?
return out
[docs]
def idot(
self,
v: BlockVector | PolarVector,
out: StencilVector | PolarVector,
):
assert v.space == self._domain
assert out.space == self._codomain
if isinstance(self.domain, PolarDerhamSpace):
out += v.tp.blocks[self.dir]
else:
out += v.blocks[self.dir]
[docs]
class CoordinateInclusion(LinearOperator):
r"""
Class of inclusion operator from one component of a :class:`~feectools.linalg.block.BlockVectorSpace`.
Represent the canonical inclusion on the :math:`\mu`-th component :
.. math::
\begin{align}
I_\mu : \ &V_\mu \longrightarrow V_1 \times \ldots \times V_\mu \times \ldots \times V_n \,,
\\[2mm]
&x_\mu \mapsto \vec{x} = (0,\ldots,x_\mu,\ldots , 0) \,.
\end{align}
Parameters
----------
mu : int
The component on which to project.
V : BlockVectorSpace | PolarDerhamSpace
Codomain, out space.
Vmu : StencilVectorSpace | PolarDerhamSpace
Domain, in space, must be :math:`\mu`-th space of V.
"""
def __init__(
self,
mu: int,
V: BlockVectorSpace | PolarDerhamSpace,
Vmu: StencilVectorSpace | PolarDerhamSpace,
):
assert isinstance(mu, int)
if isinstance(V, PolarDerhamSpace):
assert V.parent_space.spaces[mu] == Vmu.parent_space
else:
assert V.spaces[mu] == Vmu
self.dir = mu
self._domain = Vmu
self._codomain = V
self._dtype = V.dtype
@property
def domain(self):
"""Domain vector space (input) of the operator."""
return self._domain
@property
def codomain(self):
"""Codomain vector space (input) of the operator."""
return self._codomain
@property
def dtype(self):
"""Datatype of the operator."""
return self._dtype
@property
def tosparse(self):
raise NotImplementedError()
@property
def toarray(self):
raise NotImplementedError()
[docs]
def transpose(self, conjugate=False):
return CoordinateProjector(self.dir, self._codomain, self._domain)
[docs]
def dot(self, v: StencilVector | PolarVector, out=None):
assert v.space == self._domain
if isinstance(self.domain, PolarDerhamSpace):
if out is not None:
assert out.space == self._codomain
out *= 0.0
else:
out = self._codomain.zeros()
out._tp._blocks[self.dir] += v.tp
else:
if out is not None:
assert out.space == self._codomain
out *= 0.0
out._blocks[self.dir] += v
else:
blocks = [sspace.zeros() for sspace in self.codomain.spaces]
blocks[self.dir] = v.copy()
out = BlockVector(self._codomain, blocks)
out.update_ghost_regions()
return out
[docs]
def idot(self, v: StencilVector | PolarVector, out: BlockVector | PolarVector):
assert v.space == self._domain
assert out.space == self._codomain
out._blocks[self.dir] += v
[docs]
def find_relative_col(col, row, Nbasis, periodic):
"""Compute the relative row position of a StencilMatrix from the global column and row positions.
Parameters
----------
col : int
Global column index.
row : int
Global row index.
Nbasis : int
Number of B(or D)-splines for this particular dimension.
periodic : bool
True if we have periodic boundary conditions in this direction, otherwise False.
Returns
-------
relativecol : int
The relative column position of col with respect to the the current row of the StencilMatrix.
"""
if not periodic:
relativecol = col - row
# In the periodic case we must account for the possible looping of the basis functions when computing the relative row postion
else:
if col <= row:
if abs(col - row) <= abs(col + Nbasis - row):
relativecol = col - row
else:
relativecol = col + Nbasis - row
else:
if abs(col - row) <= abs(col - Nbasis - row):
relativecol = col - row
else:
relativecol = col - Nbasis - row
return relativecol