File: element.py

package info (click to toggle)
fenics-dolfinx 1%3A0.10.0.post4-1exp1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 6,028 kB
  • sloc: cpp: 36,535; python: 25,391; makefile: 226; sh: 171; xml: 55
file content (364 lines) | stat: -rw-r--r-- 13,095 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# Copyright (C) 2024 Garth N. Wells and Paul T. Kühner
#
# This file is part of DOLFINx (https://www.fenicsproject.org)
#
# SPDX-License-Identifier:    LGPL-3.0-or-later
"""Finite elements."""

from functools import singledispatch

import numpy as np
import numpy.typing as npt

import basix
import basix.ufl
from dolfinx import cpp as _cpp


class CoordinateElement:
    """Coordinate element describing the geometry map for mesh cells."""

    _cpp_object: _cpp.fem.CoordinateElement_float32 | _cpp.fem.CoordinateElement_float64

    def __init__(
        self, cmap: _cpp.fem.CoordinateElement_float32 | _cpp.fem.CoordinateElement_float64
    ):
        """Create a coordinate map element.

        Note:
            This initialiser is for internal use and not normally called
            in user code. Use :func:`coordinate_element` to create a
            CoordinateElement.

        Args:
            cmap: A C++ CoordinateElement.
        """
        assert isinstance(
            cmap, _cpp.fem.CoordinateElement_float32 | _cpp.fem.CoordinateElement_float64
        )
        self._cpp_object = cmap

    @property
    def dtype(self) -> np.dtype:
        """Scalar type for the coordinate element."""
        return np.dtype(self._cpp_object.dtype)

    @property
    def dim(self) -> int:
        """Dimension of the coordinate element space.

        This is number of basis functions that span the coordinate
        space, e.g., for a linear triangle cell the dimension will be 3.
        """
        return self._cpp_object.dim

    def hash(self) -> int:
        """Hash identifier of the coordinate element."""
        return self._cpp_object.hash()

    def create_dof_layout(self) -> _cpp.fem.ElementDofLayout:
        """Compute and return the dof layout"""
        return self._cpp_object.create_dof_layout()

    def push_forward(
        self,
        X: npt.NDArray[np.float32] | npt.NDArray[np.float64],
        cell_geometry: npt.NDArray[np.float32] | npt.NDArray[np.float64],
    ) -> npt.NDArray[np.float32] | npt.NDArray[np.float64]:
        """Push points on the reference cell forward to the physical cell.

        Args:
            X: Coordinates of points on the reference cell,
                ``shape=(num_points, topological_dimension)``.
            cell_geometry: Coordinate 'degrees-of-freedom' (nodes) of
                the cell, ``shape=(num_geometry_basis_functions,
                geometrical_dimension)``. Can be created by accessing
                ``geometry.x[geometry.dofmap.cell_dofs(i)]``,

        Returns:
            Physical coordinates of the points reference points ``X``.
        """
        return self._cpp_object.push_forward(X, cell_geometry)

    def pull_back(
        self,
        x: npt.NDArray[np.float32] | npt.NDArray[np.float64],
        cell_geometry: npt.NDArray[np.float32] | npt.NDArray[np.float64],
    ) -> npt.NDArray[np.float32] | npt.NDArray[np.float64]:
        """Pull points on the physical cell back to the reference cell.

        For non-affine cells, the pull-back is a nonlinear operation.

        Args:
            x: Physical coordinates to pull back to the reference cells,
                ``shape=(num_points, geometrical_dimension)``.
            cell_geometry: Physical coordinates describing the cell,
                shape ``(num_of_geometry_basis_functions,
                geometrical_dimension)``. They can be created by accessing
                ``geometry.x[geometry.dofmap.cell_dofs(i)]``,

        Returns:
            Reference coordinates of the physical points ``x``.
        """
        return self._cpp_object.pull_back(x, cell_geometry)

    @property
    def variant(self) -> int:
        """Lagrange variant of the coordinate element.

        Note:
            The return type is an integer. A Basix enum can be
            created using ``basix.LagrangeVariant(value)``.
        """
        return self._cpp_object.variant

    @property
    def degree(self) -> int:
        """Polynomial degree of the coordinate element."""
        return self._cpp_object.degree


@singledispatch
def coordinate_element(
    celltype: _cpp.mesh.CellType,
    degree: int,
    variant=int(basix.LagrangeVariant.unset),
    dtype: npt.DTypeLike = np.float64,
):
    """Create a Lagrange CoordinateElement from element metadata.

    Coordinate elements are typically used to create meshes.

    Args:
        celltype: Cell shape
        degree: Polynomial degree of the coordinate element map.
        variant: Basix Lagrange variant (affects node placement).
        dtype: Scalar type for the coordinate element.

    Returns:
        A coordinate element.
    """
    if np.issubdtype(dtype, np.float32):
        return CoordinateElement(_cpp.fem.CoordinateElement_float32(celltype, degree, variant))
    elif np.issubdtype(dtype, np.float64):
        return CoordinateElement(_cpp.fem.CoordinateElement_float64(celltype, degree, variant))
    else:
        raise RuntimeError("Unsupported dtype.")


@coordinate_element.register(basix.finite_element.FiniteElement)
def _(e: basix.finite_element.FiniteElement):
    """Create a Lagrange CoordinateElement from a Basix finite element.

    Coordinate elements are typically used when creating meshes.

    Args:
        e: Basix finite element.

    Returns:
        A coordinate element.
    """
    try:
        return CoordinateElement(_cpp.fem.CoordinateElement_float32(e._e))
    except TypeError:
        return CoordinateElement(_cpp.fem.CoordinateElement_float64(e._e))


class FiniteElement:
    _cpp_object: _cpp.fem.FiniteElement_float32 | _cpp.fem.FiniteElement_float64

    def __init__(
        self,
        cpp_object: _cpp.fem.FiniteElement_float32 | _cpp.fem.FiniteElement_float64,
    ):
        """Creates a Python wrapper for the exported finite element class.

        Note:
            Do not use this constructor directly. Instead use
            :func:`finiteelement`.

        Args:
            The underlying cpp instance that this object will wrap.
        """
        self._cpp_object = cpp_object

    def __eq__(self, other):
        return self._cpp_object == other._cpp_object

    @property
    def dtype(self) -> np.dtype:
        """Geometry type of the Mesh that the FunctionSpace is defined
        on."""
        return self._cpp_object.dtype

    @property
    def basix_element(self) -> basix.finite_element.FiniteElement:
        """Return underlying Basix C++ element (if it exists).

        Raises:
            Runtime error if Basix element does not exist.
        """
        return self._cpp_object.basix_element

    @property
    def num_sub_elements(self) -> int:
        """Number of sub elements (for a mixed or blocked element)."""
        return self._cpp_object.num_sub_elements

    @property
    def value_shape(self) -> npt.NDArray[np.integer]:
        """Value shape of the finite element field.

        The value shape describes the shape of the finite element field,
        e.g. ``{}`` for a scalar, ``{2}`` for a vector in 2D, ``{3, 3}``
        for a rank-2 tensor in 3D, etc.
        """
        return self._cpp_object.value_shape

    @property
    def interpolation_points(self) -> npt.NDArray[np.floating]:
        """Points on the reference cell at which an expression needs to be
        evaluated in order to interpolate the expression in the finite
        element space.

        Interpolation point coordinates on the reference cell, returning
        the coordinates data (row-major) storage with shape
        ``(num_points, tdim)``.

        Note:
            For Lagrange elements the points will just be the nodal
            positions. For other elements the points will typically be the
            quadrature points used to evaluate moment degrees of freedom.
        """
        return self._cpp_object.interpolation_points()

    @property
    def interpolation_ident(self) -> bool:
        """Check if interpolation into the finite element space is an
        identity operation given the evaluation on an expression at
        specific points, i.e. the degree-of-freedom are equal to point
        evaluations. The function will return `true` for Lagrange
        elements."""
        return self._cpp_object.interpolation_ident

    @property
    def space_dimension(self) -> int:
        """Dimension of the finite element function space (the number of
        degrees-of-freedom for the element).

        For 'blocked' elements, this function returns the dimension of the
        full element rather than the dimension of the base element.
        """
        return self._cpp_object.space_dimension

    @property
    def needs_dof_transformations(self) -> bool:
        """Check if DOF transformations are needed for this element.

        DOF transformations will be needed for elements which might not be
        continuous when two neighbouring cells disagree on the orientation
        of a shared sub-entity, and when this cannot be corrected for by
        permuting the DOF numbering in the dofmap.

        For example, Raviart-Thomas elements will need DOF transformations,
        as the neighbouring cells may disagree on the orientation of a
        basis function, and this orientation cannot be corrected for by
        permuting the DOF numbers on each cell.
        """
        return self._cpp_object.needs_dof_transformations

    @property
    def signature(self) -> str:
        """String identifying the finite element."""
        return self._cpp_object.signature

    def T_apply(
        self, x: npt.NDArray[np.floating], cell_permutations: npt.NDArray[np.uint32], dim: int
    ) -> None:
        """Transform basis functions from the reference element ordering
        and orientation to the globally consistent physical element
        ordering and orientation.

        Args:
            x: Data to transform (in place). The shape is
                ``(num_cells, n, dim)``, where ``n`` is the number degrees-
                of-freedom and the data is flattened (row-major).
            cell_permutations: Permutation data for the cell.
            dim: Number of columns in ``data``.

        Note:
            Exposed for testing. Function is not vectorised across multiple
            cells. Please see `basix.numba_helpers` for performant
            versions.
        """
        self._cpp_object.T_apply(x, cell_permutations, dim)

    def Tt_apply(
        self, x: npt.NDArray[np.floating], cell_permutations: npt.NDArray[np.uint32], dim: int
    ) -> None:
        """Apply the transpose of the operator applied by T_apply().

        Args:
            x: Data to transform (in place). The shape is
                ``(num_cells, n, dim)``, where ``n`` is the number degrees-
                of-freedom and the data is flattened (row-major).
            cell_permutations: Permutation data for the cells
            dim: Number of columns in ``data``.
        """
        self._cpp_object.Tt_apply(x, cell_permutations, dim)

    def Tt_inv_apply(
        self, x: npt.NDArray[np.floating], cell_permutations: npt.NDArray[np.uint32], dim: int
    ) -> None:
        """Apply the inverse transpose of the operator applied by
        T_apply().

        Args:
            x: Data to transform (in place). The shape is
                ``(num_cells, n, dim)``, where ``n`` is the number degrees-
                of-freedom and the data is flattened (row-major).
            cell_permutations: Permutation data for the cells
            dim: Number of columns in ``data``.
        """
        self._cpp_object.Tt_inv_apply(x, cell_permutations, dim)


def finiteelement(
    cell_type: _cpp.mesh.CellType,
    ufl_e: basix.ufl._ElementBase,
    FiniteElement_dtype: np.dtype,
) -> FiniteElement:
    """Create a DOLFINx element from a basix.ufl element.

    Args:
        cell_type: Element cell type, see ``mesh.CellType``
        ufl_e: UFL element, holding quadrature rule and other properties of
            the selected element.
        FiniteElement_dtype: Geometry type of the element.
    """
    if np.issubdtype(FiniteElement_dtype, np.float32):
        CppElement = _cpp.fem.FiniteElement_float32
    elif np.issubdtype(FiniteElement_dtype, np.float64):
        CppElement = _cpp.fem.FiniteElement_float64
    else:
        raise ValueError(f"Unsupported dtype: {FiniteElement_dtype}")

    if ufl_e.is_mixed:
        elements = [
            finiteelement(cell_type, e, FiniteElement_dtype)._cpp_object  # type: ignore
            for e in ufl_e.sub_elements
        ]
        return FiniteElement(CppElement(elements))
    elif ufl_e.is_quadrature:
        return FiniteElement(
            CppElement(
                cell_type,
                ufl_e.custom_quadrature()[0],
                ufl_e.reference_value_shape,
                ufl_e.is_symmetric,
            )
        )
    else:
        basix_e = ufl_e.basix_element._e
        value_shape = ufl_e.reference_value_shape if ufl_e.block_size > 1 else None
        return FiniteElement(CppElement(basix_e, value_shape, ufl_e.is_symmetric))